/*****************************************************************************/ /* 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 #include #include #include #include /* VMS related header files */ #include #include /* 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 [] = "Instance Cache:\ \n\ \ \n\ \n\ \n\ \n\ \n\ \n\
Size:!UL records of !UL bytes
Current:!UL
Full:!UL
Hits:!UL
Misses:!UL
Timeouts:!UL
\n\ \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 */ /************************/ /*****************************************************************************/