/*****************************************************************************/
/*
                               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
---------------
03-AUG-2016  MGD  OpenSSL v1.1.0(-pre6) required a minor code tweak
14-FEB-2008  MGD  if the entry has not timed-out then the cache was full
22-MAY-2007  JPP  bugfix; SesolaCacheGblSecInit()
11-APR-2007  MGD  make instance cache same size as session cache
11-MAY-2006  MGD  bugfix; non-SSL SesolaCacheInit() should return not bugcheck!
22-APR-2006  MGD  bugfix; SesolaCacheAddRecord() record count increment
28-MAR-2006  MGD  SesolaCacheInit() in conjunction with AuthConfigInit()
                    noting the presence of any X509 realm, automatically
                    adjusts multi-instance, SSL session cache record size
                    to accomodate potential client certificate
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_V7
#undef _VMS__V6__SOURCE
#define _VMS__V6__SOURCE
#undef __VMS_VER
#define __VMS_VER 70000000
#undef __CRTL_VER
#define __CRTL_VER 70000000
#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 */
/********************/

extern BOOL AuthRealmX509;

extern int  ExitStatus,
            GblPageCount,
            GblSectionCount,
            HttpdTickSecond,
            InstanceEnvNumber,
            InstanceNodeConfig,
            ProtocolHttpsConfigured,
            SesolaGblSecVersion,
            SesolaSessionCacheSize,
            SesolaSessionCacheTimeout;

extern unsigned long  GblSecPrvMask [];

extern char  ErrorSanityCheck[],
             SoftwareID[];

extern BIO  *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 (WATCHALL, WATCH_MOD_SESOLA, "SesolaCacheInit()");

   if (InstanceNodeConfig <= 1) return;
   if (!SesolaSessionCacheSize) return;

   /* if there was no SSL service configured */
   if (!ProtocolHttpsConfigured)  return;

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

   if (!SesolaCacheRecordSize)
      if (AuthRealmX509)
         SesolaCacheRecordSize = SESOLA_DEFAULT_CACHE_RECORD_X509;
      else
         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 (WATCHALL, WATCH_MOD_SESOLA, "SesolaCacheGblSecInit()");

   CacheRecordPoolSize = SesolaCacheRecordSize * SesolaCacheRecordMax;
   SesolaGblSecSize = sizeof(SESOLA_GBLSEC) + CacheRecordPoolSize;
   SesolaGblSecPages = SesolaGblSecSize / 512;
   if (SesolaGblSecSize & 0x1ff) SesolaGblSecPages++;

   FaoToBuffer (GblSecName, sizeof(GblSecName), &ShortLength,
                GBLSEC_NAME_FAO, HTTPD_NAME, SESOLA_GBLSEC_VERSION_NUMBER,
                InstanceEnvNumber, "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 (WATCHALL, 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];
      FaoToBuffer (String, sizeof(String), NULL,
                   "1 global section, !UL global pages", SesolaGblSecPages);
      ErrorExitVmsStatus (status, String, FI_LI);
   }

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (WATCHALL, WATCH_MOD_AUTH,
         "GBLSEC \"!AZ\" page(let)s:!UL !&S",
         GblSecName, PageCount, status);

   FaoToStdout ("%HTTPD-I-SSL, \
session cache for !UL records of !UL bytes \
in !AZ global section of !UL page(let)s\n",
                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->SinceTime64);
   }

   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 (WATCHALL, WATCH_MOD_SESOLA,
                 "SesolaCacheAddRecord() !UL",
                 SesolaGblSecPtr->CacheRecordCount);

   idptr = SSL_SESSION_get_id (SessionPtr, &idlen);

   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 (!MATCH4 (idptr, scrptr->SessId)) continue;
      if (!MATCH0 (idptr, scrptr->SessId, idlen)) continue;
      /* yes it does exist, "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->CachedTime64,
                             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 (*(ULONGPTR)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 (NULL, 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->CachedTime64,
                                scrptr->TimeoutTickSecond);

         /* if the entry has not timed-out then the cache was full */
         if (scr2ptr->TimeoutTickSecond > HttpdTickSecond)
            SesolaGblSecPtr->CacheFullCount++;
         scrptr = scr2ptr;
         memset (scrptr, 0, SesolaCacheRecordSize);
      }
   }

   sdptr = SessData;
   datlen = i2d_SSL_SESSION (SessionPtr, &sdptr);
   if (datlen > SesolaCacheRecordSize - sizeof(SESOLA_SESSION_CREC))
   {
      char  String [256];
      FaoToBuffer (String, sizeof(String), NULL,
                   "!UL byte session too large for !UL byte cache record",
                   datlen, SesolaCacheRecordSize);
      ErrorNoticed (NULL, 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->CachedTime64);
   scrptr->TimeoutTickSecond = HttpdTickSecond + SesolaCacheTimeoutSeconds;

   if (WATCH_MODULE(WATCH_MOD_SESOLA))
   {
      char  String [1024];
      SSL_SESSION_print (SesolaBioMemPtr, SessionPtr);
      BIO_read (SesolaBioMemPtr, String, sizeof(String));
      BIO_reset (SesolaBioMemPtr);
      WatchDataFormatted ("!UL !#&h !%D\n!AZ",
                          scrptr->SessDataLength, scrptr->SessIdLength,
                          scrptr->SessId, &scrptr->CachedTime64, 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;
   int64  CachedTime64;
   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 (WATCHALL, 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 (!MATCH4 (idptr, scrptr->SessId)) continue;
      if (!MATCH0 (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->CachedTime64,
                             scrptr->TimeoutTickSecond, HttpdTickSecond);

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

      SesolaGblSecPtr->CacheHitCount++;
      datlen = scrptr->SessDataLength;
      memcpy (&SessData, &scrptr->SessData, datlen);
      CachedTime64 = scrptr->CachedTime64;
      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));
         BIO_reset (SesolaBioMemPtr);
         WatchDataFormatted ("!UL !#&h !%D\n!AZ",
                             scrptr->SessDataLength, scrptr->SessIdLength,
                             scrptr->SessId, &scrptr->CachedTime64, 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 (WATCHALL, WATCH_MOD_SESOLA,
                 "SesolaCacheRemoveRecord() !UL",
                 SesolaGblSecPtr->CacheRecordCount);

   idptr = SSL_SESSION_get_id (SessionPtr, &idlen);

   InstanceMutexLock (INSTANCE_MUTEX_SSL_CACHE);

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

   for (cnt = 0; cnt < RecordCount; cnt++)
   {
      scrptr = RecordPoolPtr + (SesolaCacheRecordSize * cnt);
      if (!MATCH4 (idptr, scrptr->SessId)) continue;
      if (!MATCH0 (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->CachedTime64);

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

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

SesolaCacheStats (REQUEST_STRUCT *rqptr)

{
   static char  StatsFao [] =
"<tr><th>Instance Cache:</th><td>\
<table class=\"rghtlft\">\n\
<tr><th>Size:</th>\
<td>!UL records of !UL bytes</td></tr>\n\
<tr><th>Current:</th><td>!UL</td></tr>\n\
<tr><th>Full:</th><td>!UL</td></tr>\n\
<tr><th>Hits:</th><td>!UL</td></tr>\n\
<tr><th>Misses:</th><td>!UL</td></tr>\n\
<tr><th>Timeouts:</th><td>!UL</td></tr>\n\
</table>\n\
</td></tr>\n";

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

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

   if (WATCHMOD (rqptr, WATCH_MOD_SESOLA))
      WatchThis (WATCHITM(rqptr), WATCH_MOD_SESOLA, "SesolaCacheStats()");

   if (InstanceNodeConfig <= 1) return;
   if (!SesolaSessionCacheSize) return;

   InstanceMutexLock(INSTANCE_MUTEX_SSL_CACHE);

   vecptr = FaoVector;
   *vecptr++ = SesolaCacheRecordMax;
   *vecptr++ = SesolaCacheRecordSize;
   *vecptr++ = SesolaGblSecPtr->CacheRecordCount;
   *vecptr++ = SesolaGblSecPtr->CacheFullCount;
   *vecptr++ = SesolaGblSecPtr->CacheHitCount;
   *vecptr++ = SesolaGblSecPtr->CacheMissCount;
   *vecptr++ = SesolaGblSecPtr->CacheTimeoutCount;
   status = FaolToNet (rqptr, StatsFao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (NULL, status, NULL, 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 ()
{
   return;
}

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

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

