/*****************************************************************************/
/*
                               SesolaCache.c

Secure Sockets Layer shared session cache.

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.

Generating SSL session data is expensive and Secure Sockets Layer and OpenSSL
endeavour to reduce the impact of this activity by identifying an individual
session via an opaque handle and caching the associated session data so that
this handle may be used to retrieve it during subsequent requests.  This
behaviour is limited to a per-process instance of OpenSSL.  Where multiple WASD
instances are sharing requests in a round-robin fashion it is highly likely
that subsequent requests will be processed by a different instance.  This will
require a new session to be generated each time the request move to a different
instance.

This module provides an inter-process OpenSSL session cache for instances
executing on the same node to share.  It uses an OpenSSL session cache
extension callback that is activated when a session is not found in OpenSSL's
internal cache.  Session data is shared between instance processes via global
section shared memory.  A very simple linear search based on the session ID is
implemented (this may be improved in later releases).

I'm indebted to the ideas for inter-process session caching contained in Ralf
Engelschall's Apache MOD_SSL package.  Without this a lot more time would have
been spent working out how it needed to be implemented by examining OpenSSL's
code.  (Though don't blame him for any of my poor practices.)

Ralf S. Engelschall
rse@engelschall.com
www.engelschall.com


VERSION HISTORY
---------------
27-JUL-2003  MGD  bugfix; SesolaCacheAddRecord() oldest tick second
08-AUG-2002  MGD  bump SESOLA_DEFAULT_CACHE_RECORD_SIZE up to 1024
06-AUG-2002  MGD  enhance global section creation
21-OCT-2001  MGD  shared session cache for "instance" support
*/
/*****************************************************************************/

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <secdef.h>

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

/* application header files */
#define SESOLA_REQUIRED
#include "Sesola.h"

#define WASD_MODULE "SESOLACACHE"

/***************************************/
#ifdef SESOLA  /* secure sockets layer */
/***************************************/

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

int  SesolaCacheRecordMax,
     SesolaCacheRecordSize,
     SesolaCacheSize,
     SesolaCacheTimeoutSeconds;

SESOLA_GBLSEC  *SesolaGblSecPtr;
int  SesolaGblSecPages,
     SesolaGblSecSize;

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

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

extern int  ExitStatus,
            GblPageCount,
            GblSectionCount,
            HttpdTickSecond,
            InstanceGroupNumber,
            SesolaGblSecVersion,
            SesolaSessionCacheSize,
            SesolaSessionCacheTimeout;

extern unsigned long  GblSecPrvMask [];

extern char  ErrorSanityCheck[],
             ServerHostPort[],
             SoftwareID[],
             Utility[];

extern BIO_METHOD  *SesolaBioMemPtr;

extern CONFIG_STRUCT  Config;
extern HTTPD_PROCESS  HttpdProcess;
extern SYS_INFO  SysInfo;
extern WATCH_STRUCT  Watch;

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

SesolaCacheInit ()

{
   int  cnt, status;
   SESOLA_SESSION_CREC  *scrptr;

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

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (NULL, FI_LI, WATCH_MOD_SESOLA, "SesolaCacheInit()");

/** TODO remove at some stage **/
   {
      char  *cptr;
      if (cptr = getenv ("SESOLA_CACHE_RECORD_MAX"))
         SesolaCacheRecordMax = atoi(cptr);
      if (cptr = getenv ("SESOLA_CACHE_RECORD_SIZE"))
         SesolaCacheRecordSize = atoi(cptr);
   }

   if (!SesolaCacheRecordMax)
      SesolaCacheRecordMax = SesolaSessionCacheSize / 4;
   if (SesolaCacheRecordMax < SESOLA_DEFAULT_CACHE_RECORD_MAX)
      SesolaCacheRecordMax = SESOLA_DEFAULT_CACHE_RECORD_MAX;

   if (!SesolaCacheRecordSize)
      SesolaCacheRecordSize = SESOLA_DEFAULT_CACHE_RECORD_SIZE;
   /* let's round it to a 64 byte chunk */
   if (SesolaCacheRecordSize % 64)
      SesolaCacheRecordSize = ((SesolaCacheRecordSize / 64) + 1) * 64;

   /* session cache timeout unit is minutes */
   if (!SesolaCacheTimeoutSeconds)
      SesolaCacheTimeoutSeconds = SesolaSessionCacheTimeout * 60;
   if (!SesolaCacheTimeoutSeconds)
      SesolaCacheTimeoutSeconds = SESOLA_DEFAULT_CACHE_TIMEOUT * 60;

   SesolaCacheGblSecInit ();
}

/*****************************************************************************/
/*
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.
*/ 

int SesolaCacheGblSecInit ()

{
   /* 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,
        CacheRecordPoolSize,
        GblSecPages,
        PageCount,
        SetPrvStatus;
   short  ShortLength;
   unsigned long  RetAddr [2];
   char  GblSecName [32];
   $DESCRIPTOR (GblSecNameDsc, GblSecName);
   SESOLA_GBLSEC  *gsptr;

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

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (NULL, FI_LI, WATCH_MOD_SESOLA, "SesolaCacheGblSecInit()");

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

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

   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, SesolaGblSecPages, 0,
                           ProtectionMask, SesolaGblSecPages);
      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;
      SesolaGblSecPtr = gsptr = (SESOLA_GBLSEC*)RetAddr[0];
      SesolaGblSecPages = PageCount;
      if (VMSnok (status) || status == SS$_CREATED) break;

      /* section already exists, break if 'same size' and version! */
      if (gsptr->GblSecVersion &&
          (gsptr->GblSecVersion == SesolaGblSecVersion ||
           gsptr->GblSecLength == SesolaGblSecSize))
         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", SesolaGblSecPages);
      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);

   WriteFaoStdout (
"%!AZ-I-SSL, session cache for !UL records of !UL bytes \
in !AZ global section of !UL page(let)s\n",
                   Utility, SesolaCacheRecordMax, SesolaCacheRecordSize,
                   status == SS$_CREATED ? "a new" : "an existing",
                   SesolaGblSecPages);

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

   GblSectionCount++;
   GblPageCount += PageCount;

   return (status);
}

/*****************************************************************************/
/*
This is an OpenSSL callback activated when OpenSSL wishes to put a session into
the external cache (when adding one to it's own internal cache).  First search
the cache for an existing record.  If found just reuse it.  Next look for an
empty record while keeping track of the oldest record.  If and empty one is
found use that, otherwise the oldest.
*/ 

int SesolaCacheAddRecord
(
SSL *SslPtr,
SSL_SESSION *SessionPtr
)
{
   int  cnt, datlen, idlen, status,
        OldestTickSecond,
        RecordCount;
   unsigned char  SessData [SSL_SESSION_MAX_DER];
   unsigned char  *idptr, *sdptr,
                  *RecordPoolPtr;
   SESOLA_SESSION_CREC  *scrptr,
                        *scr2ptr;
   
   /*********/
   /* begin */
   /*********/

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (NULL, FI_LI, WATCH_MOD_SESOLA,
                 "SesolaCacheAddRecord() !UL",
                 SesolaGblSecPtr->CacheRecordCount);

   idptr = SessionPtr->session_id;
   idlen = SessionPtr->session_id_length;

   InstanceMutexLock (INSTANCE_MUTEX_SSL_CACHE);

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

   /* does it already exist? */
   for (cnt = 0; cnt < RecordCount; cnt++)
   {
      scrptr = RecordPoolPtr + (SesolaCacheRecordSize * cnt);
      if (*(unsigned long*)idptr != *(unsigned long*)scrptr->SessId) continue;
      if (memcmp (idptr, scrptr->SessId, idlen)) continue;
      /* yes it does exist, we'll just write over the top!! */
      if (WATCH_MODULE(WATCH_MOD_SESOLA))
         WatchDataFormatted ("EXISTS !UL !#&h !%D !UL\n",
                             scrptr->SessDataLength, scrptr->SessIdLength,
                             scrptr->SessId, scrptr->CachedBinTime,
                             scrptr->TimeoutTickSecond);
      break;
   }

   OldestTickSecond = HttpdTickSecond + 999999;
   if (cnt >= RecordCount)
   {
      /* find first free or timed-out record, while checking for oldest */
      scr2ptr = NULL;
      for (cnt = 0; cnt < SesolaCacheRecordMax; cnt++)
      {
         scrptr = RecordPoolPtr + (SesolaCacheRecordSize * cnt);
         if (*(unsigned long*)scrptr->SessId &&
             scrptr->TimeoutTickSecond > HttpdTickSecond)
         {
            if (scrptr->TimeoutTickSecond < OldestTickSecond)
            {
               OldestTickSecond = scrptr->TimeoutTickSecond;
               scr2ptr = scrptr;
            }
            continue;
         }
         /* if it's the first use of the record */
         if (!scrptr->TimeoutTickSecond) SesolaGblSecPtr->CacheRecordCount++;
         break;
      }

      if (cnt >= SesolaCacheRecordMax)
      {
         /* all entries in use, reuse the least recently accessed */
         if (!scr2ptr)
         {
            ErrorNoticed (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
            InstanceMutexUnLock (INSTANCE_MUTEX_SSL_CACHE);
            return (0);
         }

         if (WATCH_MODULE(WATCH_MOD_SESOLA))
            WatchDataFormatted ("REUSE !UL !#&h !%D !UL\n",
                                scr2ptr->SessDataLength, scr2ptr->SessIdLength,
                                scr2ptr->SessId, scr2ptr->CachedBinTime,
                                scrptr->TimeoutTickSecond);

         SesolaGblSecPtr->CacheReuseCount++;
         scrptr = scr2ptr;
         memset (scrptr, 0, SesolaCacheRecordSize);
      }
   }

   sdptr = SessData;
   datlen = i2d_SSL_SESSION (SessionPtr, &sdptr);
   if (datlen > SesolaCacheRecordSize - sizeof(SESOLA_SESSION_CREC))
   {
      char  String [256];
      WriteFao (String, sizeof(String), NULL,
                "!UL byte session too large for !UL byte cache record",
                datlen, SesolaCacheRecordSize);
      ErrorNoticed (SS$_RESULTOVF, String, FI_LI);
      InstanceMutexUnLock (INSTANCE_MUTEX_SSL_CACHE);
      return (0);
   }
   scrptr->SessDataLength = datlen;
   memcpy (&scrptr->SessData, SessData, datlen);
   scrptr->SessIdLength = idlen;
   memcpy (&scrptr->SessId, idptr, idlen);
   sys$gettim (&scrptr->CachedBinTime);
   scrptr->TimeoutTickSecond = HttpdTickSecond + SesolaCacheTimeoutSeconds;

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
   {
      char  String [1024];
      SSL_SESSION_print (SesolaBioMemPtr, SessionPtr);
      bio_read (SesolaBioMemPtr, String, sizeof(String));
      WatchDataFormatted ("!UL !#&h !%D\n!AZ",
                          scrptr->SessDataLength, scrptr->SessIdLength,
                          scrptr->SessId, scrptr->CachedBinTime, String);
   }

   InstanceMutexUnLock (INSTANCE_MUTEX_SSL_CACHE);

   return (0);
}

/*****************************************************************************/
/*
This is an OpenSSL callback activated when OpenSSL does not find a session in
it's own internal cache.  Search for the record identified by 'idptr'.  If not
found return a NULL.  If found un-stream the stored session data into a new
session structure and return a pointer to that.
*/ 

SSL_SESSION* SesolaCacheFindRecord
(
SSL *SslPtr,
unsigned char *idptr,
int idlen,
int *CopyPtr
)
{
   int  cnt, datlen, status,
        RecordCount;
   unsigned int  CachedBinTime [2];
   unsigned char  SessData [SSL_SESSION_MAX_DER];
   unsigned char  *sdptr,
                  *RecordPoolPtr;
   SSL_SESSION  *SessionPtr;
   SESOLA_SESSION_CREC  *scrptr;
   
   /*********/
   /* begin */
   /*********/

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (NULL, FI_LI, WATCH_MOD_SESOLA,
                 "SesolaCacheFindRecord() !UL !#&h",
                 SesolaGblSecPtr->CacheRecordCount, idlen, idptr);

   /* we have no reference to any session returned */
   *CopyPtr = 0;

   InstanceMutexLock (INSTANCE_MUTEX_SSL_CACHE);

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

   for (cnt = 0; cnt < RecordCount; cnt++)
   {
      scrptr = RecordPoolPtr + (SesolaCacheRecordSize * cnt);
      if (*(unsigned long*)idptr != *(unsigned long*)scrptr->SessId) continue;
      if (memcmp (idptr, scrptr->SessId, idlen)) continue;
      break;
   }
   if (cnt < RecordCount)
   {
      if (WATCH_MODULE(WATCH_MOD_SESOLA))
         WatchDataFormatted ("HIT!&?-TIMEOUT\r\r !UL !#&h !%D !UL !UL\n",
                             HttpdTickSecond > scrptr->TimeoutTickSecond,
                             scrptr->SessDataLength, scrptr->SessIdLength,
                             scrptr->SessId, scrptr->CachedBinTime,
                             scrptr->TimeoutTickSecond, HttpdTickSecond);

      if (HttpdTickSecond > scrptr->TimeoutTickSecond)
      {
         /* session has timed-out */
         SesolaGblSecPtr->CacheTimeoutCount++;
         /* indicate an invalid session with a leading longword of zero */
         *(unsigned long*)scrptr->SessId = 0;
         InstanceMutexUnLock (INSTANCE_MUTEX_SSL_CACHE);
         return (NULL);
      }

      SesolaGblSecPtr->CacheHitCount++;
      datlen = scrptr->SessDataLength;
      memcpy (&SessData, &scrptr->SessData, datlen);
      memcpy (&CachedBinTime, &scrptr->CachedBinTime, 8);
      InstanceMutexUnLock (INSTANCE_MUTEX_SSL_CACHE);

      sdptr = SessData;
      SessionPtr = d2i_SSL_SESSION (NULL, &sdptr, datlen);

      if (WATCH_MODULE(WATCH_MOD_SESOLA))
      {
         char  String [1024];
         SSL_SESSION_print (SesolaBioMemPtr, SessionPtr);
         bio_read (SesolaBioMemPtr, String, sizeof(String));
         WatchDataFormatted ("!UL !#&h !%D\n!AZ",
                             scrptr->SessDataLength, scrptr->SessIdLength,
                             scrptr->SessId, scrptr->CachedBinTime, String);
      }

      return (SessionPtr);
   }

   SesolaGblSecPtr->CacheMissCount++;
   InstanceMutexUnLock (INSTANCE_MUTEX_SSL_CACHE);
   return (NULL);
}

/*****************************************************************************/
/*
Search for the record identified by 'SessionPtr' session id.  If found zero the
first longword of the cached ID to indicate it's no longer in use.
*/ 

SesolaCacheRemoveRecord
(
SSL_CTX *SslCtx,
SSL_SESSION *SessionPtr
)
{
   int  cnt, idlen, status,
        RecordCount;
   unsigned char  *idptr,
                  *RecordPoolPtr;
   SESOLA_SESSION_CREC  *scrptr;
   
   /*********/
   /* begin */
   /*********/

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (NULL, FI_LI, WATCH_MOD_SESOLA,
                 "SesolaCacheRemoveRecord() !UL",
                 SesolaGblSecPtr->CacheRecordCount);

   idptr = SessionPtr->session_id;
   idlen = SessionPtr->session_id_length;

   InstanceMutexLock (INSTANCE_MUTEX_SSL_CACHE);

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

   for (cnt = 0; cnt < RecordCount; cnt++)
   {
      scrptr = RecordPoolPtr + (SesolaCacheRecordSize * cnt);
      if (*(unsigned long*)idptr != *(unsigned long*)scrptr->SessId) continue;
      if (memcmp (idptr, scrptr->SessId, idlen)) continue;
      break;
   }
   if (cnt >= RecordCount)
   {
      InstanceMutexUnLock (INSTANCE_MUTEX_SSL_CACHE);
      return;
   }

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchDataFormatted ("!UL !#&h !%D",
                          scrptr->SessDataLength, scrptr->SessIdLength,
                          scrptr->SessId, scrptr->CachedBinTime);

   /* indicate an invalid session with a leading longword of zero */
   *(unsigned long*)scrptr->SessId = 0;
   InstanceMutexUnLock (INSTANCE_MUTEX_SSL_CACHE);
}

/*****************************************************************************/
/*
Called by SesolaReport().
*/

SesolaCacheStats (REQUEST_STRUCT *rqptr)

{
   static char  StatsFao [] =
"<TR><TH ALIGN=right VALIGN=top>Instance&nbsp;Cache:</TH><TD VALIGN=top>\
<TABLE CELLPADDING=0 CELLSPACING=0 BORDER=0>\n\
<TR><TH ALIGN=right>Size:</TH>\
<TD ALIGN=left> &nbsp;!UL records of !UL bytes</TD></TR>\n\
<TR><TH ALIGN=right>Current:</TH><TD ALIGN=left> &nbsp;!UL</TD></TR>\n\
<TR><TH ALIGN=right>Full:</TH><TD ALIGN=left> &nbsp;!UL</TD></TR>\n\
<TR><TH ALIGN=right>Hits:</TH><TD ALIGN=left> &nbsp;!UL</TD></TR>\n\
<TR><TH ALIGN=right>Misses:</TH><TD ALIGN=left> &nbsp;!UL</TD></TR>\n\
<TR><TH ALIGN=right>&nbsp;Timeouts:</TH><TD ALIGN=left> &nbsp;!UL</TD></TR>\n\
</TABLE>\n\
</TD></TR>\n";

   int  status;
   unsigned long  FaoVector [16];
   unsigned long  *vecptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_SESOLA))
      WatchThis (rqptr, FI_LI, WATCH_MOD_SESOLA, "SesolaCacheStats()");

   InstanceMutexLock(INSTANCE_MUTEX_SSL_CACHE);

   vecptr = FaoVector;
   *vecptr++ = SesolaCacheRecordMax;
   *vecptr++ = SesolaCacheRecordSize;
   *vecptr++ = SesolaGblSecPtr->CacheRecordCount;
   *vecptr++ = SesolaGblSecPtr->CacheReuseCount;
   *vecptr++ = SesolaGblSecPtr->CacheHitCount;
   *vecptr++ = SesolaGblSecPtr->CacheMissCount;
   *vecptr++ = SesolaGblSecPtr->CacheTimeoutCount;
   status = NetWriteFaol (rqptr, StatsFao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

   InstanceMutexUnLock(INSTANCE_MUTEX_SSL_CACHE);
}

/*****************************************************************************/
/*
For compilations without SSL these functions provide LINKage stubs for the
rest of the HTTPd modules, allowing for just recompiling the Sesola module to
integrate the SSL functionality.
*/

/*********************/
#else  /* not SESOLA */
/*********************/

/* external storage */
extern char  ErrorSanityCheck[];
extern WATCH_STRUCT  Watch;

SesolaCacheInit ()
{
   ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
}

/************************/
#endif  /* ifdef SESOLA */
/************************/

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

