windows-nt/Source/XPSP1/NT/ds/security/protocols/schannel/spbase/cache.c

1808 lines
41 KiB
C
Raw Permalink Normal View History

2020-09-26 03:20:57 -05:00
//+---------------------------------------------------------------------------
//
// Microsoft Windows
// Copyright (C) Microsoft Corporation, 1992 - 1995.
//
// File: cache.c
//
// Contents:
//
// Classes:
//
// Functions:
//
// History: 09-23-97 jbanes LSA integration stuff.
// 07-31-98 jbanes Made thread-safe.
//
//----------------------------------------------------------------------------
#include "spbase.h"
#include <limits.h>
#include <mapper.h>
#include <sslcache.h>
SCHANNEL_CACHE SchannelCache =
{
NULL, // SessionCache
SP_CACHE_CLIENT_LIFESPAN, // dwClientLifespan
SP_CACHE_SERVER_LIFESPAN, // dwServerLifespan
SP_CACHE_CLEANUP_INTERVAL, // dwCleanupInterval
SP_MAXIMUM_CACHE_ELEMENTS, // dwCacheSize
SP_MAXIMUM_CACHE_ELEMENTS, // dwMaximumEntries
0 // dwUsedEntries
};
RTL_CRITICAL_SECTION g_CacheCleanupLock;
BOOL g_CacheCleanupCritSectInitialized = FALSE;
LIST_ENTRY g_CacheCleanupList;
DWORD g_CacheCleanupCount = 0;
HANDLE g_CacheCleanupEvent = NULL;
HANDLE g_CacheCleanupWaitObject = NULL;
BOOL g_fMultipleProcessClientCache = FALSE;
BOOL g_fCacheInitialized = FALSE;
// Perf counter values.
DWORD g_cClientHandshakes = 0;
DWORD g_cServerHandshakes = 0;
DWORD g_cClientReconnects = 0;
DWORD g_cServerReconnects = 0;
BOOL
SPCacheDelete(
PSessCacheItem pItem);
BOOL
CacheExpireElements(
BOOL fCleanupOnly,
BOOL fBackground);
VOID
CacheCleanupHandler(
PVOID pVoid,
BOOLEAN fTimeout);
SP_STATUS
SPInitSessionCache(VOID)
{
DWORD i;
NTSTATUS Status = STATUS_SUCCESS;
SP_BEGIN("SPInitSessionCache");
//
// Allocate memory for cache, and initialize synchronization resource.
//
InitializeListHead(&SchannelCache.EntryList);
RtlInitializeResource(&SchannelCache.Lock);
SchannelCache.LockInitialized = TRUE;
SchannelCache.SessionCache = (PLIST_ENTRY)SPExternalAlloc(SchannelCache.dwCacheSize * sizeof(LIST_ENTRY));
if(SchannelCache.SessionCache == NULL)
{
Status = SP_LOG_RESULT(STATUS_NO_MEMORY);
goto cleanup;
}
for(i = 0; i < SchannelCache.dwCacheSize; i++)
{
InitializeListHead(&SchannelCache.SessionCache[i]);
}
DebugLog((DEB_TRACE, "Space reserved at 0x%x for %d cache entries.\n",
SchannelCache.SessionCache,
SchannelCache.dwCacheSize));
//
// Initialize cache cleanup objects.
//
InitializeListHead(&g_CacheCleanupList);
Status = RtlInitializeCriticalSection(&g_CacheCleanupLock);
if(!NT_SUCCESS(Status))
{
goto cleanup;
}
g_CacheCleanupCritSectInitialized = TRUE;
g_CacheCleanupEvent = CreateEvent(NULL,
FALSE,
FALSE,
NULL);
if(NULL == g_CacheCleanupEvent)
{
Status = GetLastError();
goto cleanup;
}
if(!RegisterWaitForSingleObject(&g_CacheCleanupWaitObject,
g_CacheCleanupEvent,
CacheCleanupHandler,
NULL,
SchannelCache.dwCleanupInterval,
WT_EXECUTEDEFAULT))
{
Status = GetLastError();
goto cleanup;
}
g_fCacheInitialized = TRUE;
Status = STATUS_SUCCESS;
cleanup:
if(!NT_SUCCESS(Status))
{
SPShutdownSessionCache();
}
SP_RETURN(Status);
}
SP_STATUS
SPShutdownSessionCache(VOID)
{
PSessCacheItem pItem;
PLIST_ENTRY pList;
DWORD i;
SP_BEGIN("SPShutdownSessionCache");
if(SchannelCache.LockInitialized)
{
RtlAcquireResourceExclusive(&SchannelCache.Lock, TRUE);
}
g_fCacheInitialized = FALSE;
if(SchannelCache.SessionCache != NULL)
{
// Blindly kill all cache items.
// No contexts should be running at
// this time.
pList = SchannelCache.EntryList.Flink;
while(pList != &SchannelCache.EntryList)
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, EntryList.Flink);
pList = pList->Flink;
SPCacheDelete(pItem);
}
SPExternalFree(SchannelCache.SessionCache);
}
if(g_CacheCleanupCritSectInitialized)
{
RtlDeleteCriticalSection(&g_CacheCleanupLock);
g_CacheCleanupCritSectInitialized = FALSE;
}
if(g_CacheCleanupWaitObject)
{
UnregisterWaitEx(g_CacheCleanupWaitObject, INVALID_HANDLE_VALUE);
g_CacheCleanupWaitObject = NULL;
}
if(g_CacheCleanupEvent)
{
CloseHandle(g_CacheCleanupEvent);
g_CacheCleanupEvent = NULL;
}
if(SchannelCache.LockInitialized)
{
RtlDeleteResource(&SchannelCache.Lock);
SchannelCache.LockInitialized = FALSE;
}
SP_RETURN(PCT_ERR_OK);
}
LONG
SPCacheReference(
PSessCacheItem pItem)
{
LONG cRet;
if(pItem == NULL)
{
return -1;
}
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
cRet = InterlockedIncrement(&pItem->cRef);
return cRet;
}
LONG
SPCacheDereference(PSessCacheItem pItem)
{
long cRet;
if(pItem == NULL)
{
return -1;
}
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
cRet = InterlockedDecrement(&pItem->cRef);
ASSERT(cRet > 0);
return cRet;
}
BOOL
SPCacheDelete(
PSessCacheItem pItem)
{
long cRet;
DebugLog((DEB_TRACE, "Delete cache item:0x%x\n", pItem));
if(pItem == NULL)
{
return FALSE;
}
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
pItem->pActiveServerCred = NULL;
pItem->pServerCred = NULL;
if(pItem->hMasterKey)
{
if(!CryptDestroyKey(pItem->hMasterKey))
{
SP_LOG_RESULT(GetLastError());
}
pItem->hMasterKey = 0;
}
if(pItem->pRemoteCert)
{
CertFreeCertificateContext(pItem->pRemoteCert);
pItem->pRemoteCert = NULL;
}
if(pItem->pRemotePublic)
{
SPExternalFree(pItem->pRemotePublic);
pItem->pRemotePublic = NULL;
}
if(pItem->phMapper)
{
if(pItem->hLocator)
{
SslCloseLocator(pItem->phMapper, pItem->hLocator);
pItem->hLocator = 0;
}
SslDereferenceMapper(pItem->phMapper);
}
pItem->phMapper = NULL;
if(pItem->pbServerCertificate)
{
SPExternalFree(pItem->pbServerCertificate);
pItem->pbServerCertificate = NULL;
pItem->cbServerCertificate = 0;
}
if(pItem->szCacheID)
{
SPExternalFree(pItem->szCacheID);
pItem->szCacheID = NULL;
}
if(pItem->pClientCred)
{
SPDeleteCred(pItem->pClientCred);
pItem->pClientCred = NULL;
}
if(pItem->pClientCert)
{
CertFreeCertificateContext(pItem->pClientCert);
pItem->pClientCert = NULL;
}
if(pItem->pClonedItem)
{
SPCacheDereference(pItem->pClonedItem);
pItem->pClonedItem = NULL;
}
if(pItem->pbAppData)
{
SPExternalFree(pItem->pbAppData);
pItem->pbAppData = NULL;
}
SPExternalFree(pItem);
return TRUE;
}
void
SPCachePurgeCredential(
PSPCredentialGroup pCred)
{
PSessCacheItem pItem;
PLIST_ENTRY pList;
DWORD i;
//
// Only server credentials are bound to the cache, so return if this is
// a client credential.
//
if(pCred->grbitProtocol & SP_PROT_CLIENTS)
{
return;
}
//
// Search through the cache entries looking for entries that are
// bound to the specified server credential.
//
RtlAcquireResourceShared(&SchannelCache.Lock, TRUE);
pList = SchannelCache.EntryList.Flink;
while(pList != &SchannelCache.EntryList)
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, EntryList.Flink);
pList = pList->Flink;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
// Is this a server cache entry?
if((pItem->fProtocol & SP_PROT_SERVERS) == 0)
{
continue;
}
// Does this item match the current credentials?
if(!IsSameThumbprint(&pCred->CredThumbprint, &pItem->CredThumbprint))
{
continue;
}
// Mark this entry as non-resumable. This will cause the entry to
// be deleted automatically by the cleanup routines.
pItem->ZombieJuju = FALSE;
pItem->DeferredJuju = FALSE;
}
RtlReleaseResource(&SchannelCache.Lock);
//
// Delete all unused non-resumable cache entries.
//
CacheExpireElements(FALSE, FALSE);
}
void
SPCachePurgeProcessId(
ULONG ProcessId)
{
PSessCacheItem pItem;
PLIST_ENTRY pList;
DWORD i;
//
// Search through the cache entries looking for entries that are
// bound to the specified process.
//
RtlAcquireResourceShared(&SchannelCache.Lock, TRUE);
pList = SchannelCache.EntryList.Flink;
while(pList != &SchannelCache.EntryList)
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, EntryList.Flink);
pList = pList->Flink;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
// Does this item match the specified process?
if(pItem->ProcessID != ProcessId)
{
continue;
}
// Mark the entry as ownerless.
pItem->ProcessID = 0;
// Mark this entry as non-resumable. This will cause the entry to
// be deleted automatically by the cleanup routines.
pItem->ZombieJuju = FALSE;
pItem->DeferredJuju = FALSE;
}
RtlReleaseResource(&SchannelCache.Lock);
//
// Delete all unused non-resumable cache entries.
//
CacheExpireElements(FALSE, FALSE);
}
BOOL
IsSameTargetName(
LPWSTR Name1,
LPWSTR Name2)
{
if(Name1 == Name2)
{
return TRUE;
}
if(Name1 == NULL || Name2 == NULL || wcscmp(Name1, Name2) != 0)
{
return FALSE;
}
return TRUE;
}
BOOL
DoesAppAllowCipher(
PSPCredentialGroup pCredGroup,
PSessCacheItem pItem)
{
PKeyExchangeInfo pExchInfo;
if(pCredGroup == NULL)
{
return FALSE;
}
//
// Is protocol supported?
//
if((pItem->fProtocol & pCredGroup->grbitEnabledProtocols) == 0)
{
return FALSE;
}
//
// Is cipher supported?
//
if(pItem->dwStrength < pCredGroup->dwMinStrength)
{
return FALSE;
}
if(pItem->dwStrength > pCredGroup->dwMaxStrength)
{
return FALSE;
}
if(!IsAlgAllowed(pCredGroup, pItem->aiCipher))
{
return FALSE;
}
//
// Is hash supported?
//
if(!IsAlgAllowed(pCredGroup, pItem->aiHash))
{
return FALSE;
}
//
// Is exchange alg supported?
//
if(pItem->SessExchSpec != SP_EXCH_UNKNOWN)
{
pExchInfo = GetKeyExchangeInfo(pItem->SessExchSpec);
if(pExchInfo == NULL)
{
return FALSE;
}
if((pExchInfo->fProtocol & pItem->fProtocol) == 0)
{
return FALSE;
}
if(!IsAlgAllowed(pCredGroup, pExchInfo->aiExch))
{
return FALSE;
}
}
return TRUE;
}
BOOL SPCacheRetrieveBySession(
struct _SPContext * pContext,
PBYTE pbSessionID,
DWORD cbSessionID,
PSessCacheItem *ppRetItem)
{
DWORD index;
DWORD timeNow;
ULONG ProcessID;
PSessCacheItem pItem;
PLIST_ENTRY pList;
BOOL fFound = FALSE;
DebugLog((DEB_TRACE, "SPCacheRetrieveBySession (%x) called\n", pContext));
if(ppRetItem == NULL)
{
return FALSE;
}
//
// Compute the cache index.
//
if(cbSessionID < sizeof(DWORD))
{
DebugLog((DEB_TRACE, " FAILED\n"));
return FALSE;
}
CopyMemory((PBYTE)&index, pbSessionID, sizeof(DWORD));
if(index >= SchannelCache.dwCacheSize)
{
DebugLog((DEB_TRACE, " FAILED\n"));
return FALSE;
}
//
// Retrieve the current time and application process id.
//
timeNow = GetTickCount();
SslGetClientProcess(&ProcessID);
//
// Lock the cache for read.
//
RtlAcquireResourceShared(&SchannelCache.Lock, TRUE);
//
// Search through the cache entries at the computed index.
//
pList = SchannelCache.SessionCache[index].Flink;
while(pList != &SchannelCache.SessionCache[index])
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, IndexEntryList.Flink);
pList = pList->Flink ;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
// Is this entry resumable?
if(!pItem->ZombieJuju)
{
continue;
}
// Has this item expired?
if(HasTimeElapsed(pItem->CreationTime, timeNow, pItem->Lifespan))
{
continue;
}
// Does the session id match?
if(cbSessionID != pItem->cbSessionID)
{
continue;
}
if(memcmp(pbSessionID, pItem->SessionID, cbSessionID) != 0)
{
continue;
}
// Is this item for the protocol we're using.
if(0 == (pContext->dwProtocol & pItem->fProtocol))
{
continue;
}
// Does this item belong to our client process?
if(pItem->ProcessID != ProcessID)
{
continue;
}
// Does this item match the current server credentials?
//
// We don't allow different server credentials to share cache
// entries, because if the credential that was used during
// the original full handshake is deleted, then the cache
// entry is unusable. Some server applications (I won't name names)
// create a new credential for each connection, and we have to
// guard against this.
//
// Note that this restriction may result in an extra full
// handshake when IE accesses an IIS site enabled for certificate
// mapping, mostly because IE's behavior is broken.
if(!IsSameThumbprint(&pContext->pCredGroup->CredThumbprint,
&pItem->CredThumbprint))
{
continue;
}
// Make sure that the application supports the cipher suite
// used by this cache entry. This becomes important now that
// we're allowing different server credentials to share
// cache entries.
if(!DoesAppAllowCipher(pContext->pCredGroup, pItem))
{
continue;
}
//
// Found item in cache!!
//
fFound = TRUE;
SPCacheReference(pItem);
// Are we replacing something?
// Then dereference the thing we are replacing.
if(*ppRetItem)
{
SPCacheDereference(*ppRetItem);
}
// Return item referenced.
*ppRetItem = pItem;
break;
}
RtlReleaseResource(&SchannelCache.Lock);
if(fFound)
{
DebugLog((DEB_TRACE, " FOUND IT(%u)\n", index));
InterlockedIncrement(&g_cServerReconnects);
}
else
{
DebugLog((DEB_TRACE, " FAILED\n"));
}
return fFound;
}
DWORD
ComputeClientCacheIndex(
LPWSTR pszTargetName)
{
DWORD index;
MD5_CTX Md5Hash;
DWORD cbTargetName;
if(pszTargetName == NULL)
{
index = 0;
}
else
{
cbTargetName = wcslen(pszTargetName) * sizeof(WCHAR);
MD5Init(&Md5Hash);
MD5Update(&Md5Hash,
(PBYTE)pszTargetName,
cbTargetName);
MD5Final(&Md5Hash);
CopyMemory((PBYTE)&index,
Md5Hash.digest,
sizeof(DWORD));
index %= SchannelCache.dwCacheSize;
}
return index;
}
BOOL
SPCacheRetrieveByName(
LPWSTR pszTargetName,
PSPCredentialGroup pCredGroup,
PSessCacheItem *ppRetItem)
{
DWORD index;
PSessCacheItem pItem;
PSessCacheItem pFoundEntry = NULL;
DWORD timeNow;
LUID LogonId;
PLIST_ENTRY pList;
PSPCredential pCurrentCred = NULL;
DebugLog((DEB_TRACE, "SPCacheRetrieveByName (%ls) called\n", pszTargetName));
if(ppRetItem == NULL)
{
return FALSE;
}
//
// Retrieve the current time and user logon id.
//
timeNow = GetTickCount();
SslGetClientLogonId(&LogonId);
//
// Compute the cache index.
//
index = ComputeClientCacheIndex(pszTargetName);
//
// Lock the cache for read.
//
RtlAcquireResourceShared(&SchannelCache.Lock, TRUE);
//
// Search through the cache entries at the computed index.
//
pList = SchannelCache.SessionCache[index].Flink;
while(pList != &SchannelCache.SessionCache[index])
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, IndexEntryList.Flink);
pList = pList->Flink ;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
// Is this entry resumable?
if(!pItem->ZombieJuju)
{
continue;
}
// Is this item for the protocol we're using?
if(0 == (pCredGroup->grbitEnabledProtocols & pItem->fProtocol))
{
continue;
}
// Has this item expired?
if(HasTimeElapsed(pItem->CreationTime, timeNow, pItem->Lifespan))
{
continue;
}
// Don't allow reconnects when Skipjack is used.
if(pItem->aiCipher == CALG_SKIPJACK)
{
continue;
}
// Does this item belong to our client?
if(!RtlEqualLuid(&pItem->LogonId, &LogonId))
{
continue;
}
// Does this item match our current credentials?
if(g_fMultipleProcessClientCache)
{
// If this cache entry has a client certificate associated with it
// and the passed in client credentials contain one or more certificates,
// then we need to make sure that they overlap.
if(IsValidThumbprint(&pItem->CertThumbprint) && pCredGroup->pCredList != NULL)
{
if(!DoesCredThumbprintMatch(pCredGroup, &pItem->CertThumbprint))
{
continue;
}
}
}
else
{
// Make sure the thumbprint of the credential group matches the
// thumbprint of the cache entry.
if(!IsSameThumbprint(&pCredGroup->CredThumbprint,
&pItem->CredThumbprint))
{
continue;
}
}
if(!IsSameTargetName(pItem->szCacheID, pszTargetName))
{
continue;
}
// Make sure that the application supports the cipher suite
// used by this cache entry. This becomes important in the
// multi-process client cache scenario, since different client
// applications may be running with different settings.
if(!DoesAppAllowCipher(pCredGroup, pItem))
{
continue;
}
//
// Found item in cache!!
//
if(pFoundEntry == NULL)
{
// This is the first matching entry found.
SPCacheReference(pItem);
// Remember the current entry.
pFoundEntry = pItem;
}
else
{
if(pItem->CreationTime > pFoundEntry->CreationTime)
{
// We found a newer entry.
SPCacheReference(pItem);
// Disable searching on the previous item.
pFoundEntry->ZombieJuju = FALSE;
// Release the previous item.
SPCacheDereference(pFoundEntry);
// Remember the current entry.
pFoundEntry = pItem;
}
else
{
// This item is older than the previously found entry.
// Disable searching on the current entry.
pItem->ZombieJuju = FALSE;
}
}
}
RtlReleaseResource(&SchannelCache.Lock);
if(pFoundEntry)
{
// Found item in cache!!
// Are we replacing something?
// Then dereference the thing we are replacing.
if(*ppRetItem)
{
SPCacheDereference(*ppRetItem);
}
// Return item referenced.
*ppRetItem = pFoundEntry;
DebugLog((DEB_TRACE, " FOUND IT(%u)\n", index));
InterlockedIncrement(&g_cClientReconnects);
}
else
{
DebugLog((DEB_TRACE, " FAILED\n"));
}
return (pFoundEntry != NULL);
}
BOOL
IsApplicationCertificateMapper(
PHMAPPER phMapper)
{
if(phMapper == NULL)
{
return FALSE;
}
if(phMapper->m_dwFlags & SCH_FLAG_SYSTEM_MAPPER)
{
return FALSE;
}
return TRUE;
}
//+---------------------------------------------------------------------------
//
// Function: CacheExpireElements
//
// Synopsis: Traverse the session cache and remove all expired entries.
// If the cache is oversized, then expire some entries
// early.
//
// Arguments: [fCleanupOnly] -- If this is set, then attempt to delete
// cache entries previously expired. Don't
// traverse the cache.
//
// History: 01-02-2000 jbanes Created.
//
// Notes: This routine should be called only once every five or ten
// minutes.
//
// The tricky bit is how to handle the case where the cache
// entry belongs to IIS, and has an IIS certificate mapper
// "locator" attached to it. In this case, we cannot destroy
// the cache element unless the client process is IIS, because
// we need to callback to IIS in order to destroy the locator.
// In this case, we remove the element from the cache, and
// leave it laying around in a global "cache cleanup" list.
// If this list gets too large, then this routine should be
// called frequently, with the "fCleanupOnly" parameter set
// to TRUE.
//
//----------------------------------------------------------------------------
BOOL
CacheExpireElements(
BOOL fCleanupOnly,
BOOL fBackground)
{
static ULONG RefCount = 0;
ULONG LocalRefCount;
DWORD timeNow;
ULONG ProcessID;
PSessCacheItem pItem;
PLIST_ENTRY pList;
DWORD CleanupCount;
ULONG Count;
//
// If another thread is currently expiring elements, then try again
// later.
//
LocalRefCount = InterlockedIncrement(&RefCount);
if(fBackground && LocalRefCount > 1)
{
InterlockedDecrement(&RefCount);
return FALSE;
}
RtlEnterCriticalSection(&g_CacheCleanupLock);
//
// Retrieve the current time and application process id.
//
timeNow = GetTickCount();
SslGetClientProcess(&ProcessID);
//
// Search through the cache entries looking for expired entries.
//
if(!fCleanupOnly)
{
RtlAcquireResourceExclusive(&SchannelCache.Lock, TRUE);
pList = SchannelCache.EntryList.Flink;
while(pList != &SchannelCache.EntryList)
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, EntryList.Flink);
pList = pList->Flink;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
// Is the cache entry currently being used?
if(pItem->cRef > 1)
{
continue;
}
// Mark all expired cache entries as non-resumable.
if(HasTimeElapsed(pItem->CreationTime, timeNow, pItem->Lifespan))
{
pItem->ZombieJuju = FALSE;
pItem->DeferredJuju = FALSE;
}
// If the cache has gotten too large, then expire elements early. The
// cache elements are sorted by creation time, so the oldest
// entries will be expired first.
if(SchannelCache.dwUsedEntries > SchannelCache.dwMaximumEntries)
{
pItem->ZombieJuju = FALSE;
pItem->DeferredJuju = FALSE;
}
// Don't remove entries that are still valid.
if(pItem->ZombieJuju == TRUE || pItem->DeferredJuju)
{
continue;
}
//
// Remove this entry from the cache, and add it to the list of
// entries to be destroyed.
//
RemoveEntryList(&pItem->IndexEntryList);
RemoveEntryList(&pItem->EntryList);
SchannelCache.dwUsedEntries--;
InsertTailList(&g_CacheCleanupList, &pItem->EntryList);
}
RtlReleaseResource(&SchannelCache.Lock);
}
//
// Kill the expired zombies.
//
CleanupCount = 0;
pList = g_CacheCleanupList.Flink;
while(pList != &g_CacheCleanupList)
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, EntryList.Flink);
pList = pList->Flink;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
// Make sure that we only destroy server entries that belong to the
// current application process. This is necessary because of the
// IIS certificate mapper.
if(pItem->ProcessID != 0 &&
pItem->ProcessID != ProcessID)
{
if(IsApplicationCertificateMapper(pItem->phMapper))
{
CleanupCount++;
continue;
}
}
// Remove entry from cleanup list.
RemoveEntryList(&pItem->EntryList);
// Destroy cache entry.
SPCacheDelete(pItem);
}
g_CacheCleanupCount = CleanupCount;
RtlLeaveCriticalSection(&g_CacheCleanupLock);
InterlockedDecrement(&RefCount);
return TRUE;
}
VOID
CacheCleanupHandler(
PVOID pVoid,
BOOLEAN fTimeout)
{
if(SchannelCache.dwUsedEntries > 0)
{
if(fTimeout)
{
DebugLog((DEB_WARN, "Initiate periodic cache cleanup.\n"));
}
CacheExpireElements(FALSE, TRUE);
ResetEvent(g_CacheCleanupEvent);
}
}
/* allocate a new cache item to be used
* by a context. Initialize it with the
* pszTarget if the target exists.
* Auto-Generate a SessionID
*/
BOOL
SPCacheRetrieveNew(
BOOL fServer,
LPWSTR pszTargetName,
PSessCacheItem * ppRetItem)
{
DWORD index;
DWORD timeNow;
ULONG ProcessID;
LUID LogonId;
PSessCacheItem pItem;
BYTE rgbSessionId[SP_MAX_SESSION_ID];
DebugLog((DEB_TRACE, "SPCacheRetrieveNew called\n"));
//
// Trigger cache cleanup if too many cache entries already exist.
//
if(SchannelCache.dwUsedEntries > (SchannelCache.dwMaximumEntries * 21) / 20)
{
DebugLog((DEB_WARN, "Cache size (%d) exceeded threshold (%d), trigger cache cleanup.\n",
SchannelCache.dwUsedEntries,
SchannelCache.dwMaximumEntries));
SetEvent(g_CacheCleanupEvent);
}
//
// Perform cache garbage collection when the list of entries to be
// deleted grows too large.
//
if(fServer && g_CacheCleanupCount > 50)
{
DebugLog((DEB_WARN, "Attempt background cleanup of deleted zombies.\n"));
CacheExpireElements(TRUE, TRUE);
}
//
// Retrieve the current time and user logon id.
//
timeNow = GetTickCount();
SslGetClientProcess(&ProcessID);
SslGetClientLogonId(&LogonId);
//
// Compute the session id and the cache index.
//
if(fServer)
{
GenerateRandomBits(rgbSessionId, sizeof(rgbSessionId));
index = *(DWORD *)rgbSessionId % SchannelCache.dwCacheSize;
*(DWORD *)rgbSessionId = index;
}
else
{
ZeroMemory(rgbSessionId, sizeof(rgbSessionId));
index = ComputeClientCacheIndex(pszTargetName);
}
//
// Allocate a new cache entry.
//
pItem = SPExternalAlloc(sizeof(SessCacheItem));
if(pItem == NULL)
{
SP_LOG_RESULT(SEC_E_INSUFFICIENT_MEMORY);
return FALSE;
}
//
// Fill in the cache internal fields.
//
pItem->Magic = SP_CACHE_MAGIC;
pItem->cRef = 1;
pItem->CreationTime = timeNow;
if(fServer)
{
pItem->Lifespan = SchannelCache.dwServerLifespan;
}
else
{
pItem->Lifespan = SchannelCache.dwClientLifespan;
}
pItem->ProcessID = ProcessID;
pItem->LogonId = LogonId;
#ifdef LOCK_MASTER_KEYS
pItem->csMasterKey = g_rgcsMasterKey + (index % SP_MASTER_KEY_CS_COUNT);
#endif
if(pszTargetName)
{
pItem->szCacheID = SPExternalAlloc((wcslen(pszTargetName) + 1) * sizeof(WCHAR));
if(pItem->szCacheID == NULL)
{
SP_LOG_RESULT(SEC_E_INSUFFICIENT_MEMORY);
SPExternalFree(pItem);
return FALSE;
}
wcscpy(pItem->szCacheID, pszTargetName);
}
else
{
pItem->szCacheID = NULL;
}
memcpy(pItem->SessionID, rgbSessionId, sizeof(rgbSessionId));
//
// Give the caller a reference.
//
SPCacheReference(pItem);
*ppRetItem = pItem;
//
// Add the new entry to the cache.
//
RtlAcquireResourceExclusive(&SchannelCache.Lock, TRUE);
InsertTailList(&SchannelCache.SessionCache[index], &pItem->IndexEntryList);
InsertTailList(&SchannelCache.EntryList, &pItem->EntryList);
SchannelCache.dwUsedEntries++;
RtlReleaseResource(&SchannelCache.Lock);
return TRUE;
}
BOOL
SPCacheAdd(
PSPContext pContext)
{
PSessCacheItem pItem;
PSPCredentialGroup pCred;
DWORD dwLifespan;
DWORD timeNow;
timeNow = GetTickCount();
pItem = pContext->RipeZombie;
if(!pItem) return FALSE;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
pCred = pContext->pCredGroup;
if(!pCred) return FALSE;
if(pItem->fProtocol & SP_PROT_CLIENTS)
{
dwLifespan = min(pCred->dwSessionLifespan, SchannelCache.dwClientLifespan);
}
else
{
dwLifespan = min(pCred->dwSessionLifespan, SchannelCache.dwServerLifespan);
}
// Remember which client certificate we used.
if(pItem->fProtocol & SP_PROT_CLIENTS)
{
pItem->CredThumbprint = pContext->pCredGroup->CredThumbprint;
if(pContext->pActiveClientCred)
{
pItem->CertThumbprint = pContext->pActiveClientCred->CertThumbprint;
pItem->pClientCert = CertDuplicateCertificateContext(pContext->pActiveClientCred->pCert);
if(pItem->pClientCert == NULL)
{
SP_LOG_RESULT(GetLastError());
}
}
}
// Are we supposed to defer reconnects for this connection?
if(pItem->pServerCred != NULL)
{
if(pItem->pServerCred->dwFlags & CRED_FLAG_DISABLE_RECONNECTS)
{
pItem->DeferredJuju = TRUE;
}
}
// Allow cache ownership of this item
pItem->dwFlags |= SP_CACHE_FLAG_READONLY;
if(!pItem->DeferredJuju)
{
pItem->ZombieJuju = TRUE;
}
// if we are a cloned item, abort the old
// item, and then dereference it.
if(pItem->pClonedItem)
{
pItem->pClonedItem->ZombieJuju = FALSE;
SPCacheDereference(pItem->pClonedItem);
pItem->pClonedItem = NULL;
}
pItem->Lifespan = dwLifespan;
return TRUE;
}
/* Allocate a new cache item, and copy
* over relevant information from old item,
* and dereference old item. This is a helper
* for REDO
*/
BOOL
SPCacheClone(PSessCacheItem *ppItem)
{
PSessCacheItem pNewItem;
PSessCacheItem pOldItem;
if(ppItem == NULL || *ppItem == NULL)
{
return FALSE;
}
pOldItem = *ppItem;
ASSERT(pOldItem->Magic == SP_CACHE_MAGIC);
ASSERT(!(pOldItem->fProtocol & SP_PROT_CLIENTS) || !(pOldItem->fProtocol & SP_PROT_SERVERS));
// Get a fresh cache item.
pNewItem = NULL;
if(!SPCacheRetrieveNew((pOldItem->fProtocol & SP_PROT_CLIENTS) == 0,
pOldItem->szCacheID,
&pNewItem))
{
return FALSE;
}
// Copy the master CSP prov handle.
pNewItem->hMasterProv = pOldItem->hMasterProv;
// Copy over old relevant data
pNewItem->fProtocol = pOldItem->fProtocol;
pNewItem->dwCF = pOldItem->dwCF;
pNewItem->phMapper = pOldItem->phMapper;
pNewItem->pServerCred = pOldItem->pServerCred;
pNewItem->pActiveServerCred = pOldItem->pActiveServerCred;
if(pOldItem->dwFlags & SP_CACHE_FLAG_MASTER_EPHEM)
{
pNewItem->dwFlags |= SP_CACHE_FLAG_MASTER_EPHEM;
}
pNewItem->CredThumbprint = pOldItem->CredThumbprint,
// This item will be dereferenced, and
// Aborted when the new item is completed.
pNewItem->pClonedItem = pOldItem;
*ppItem = pNewItem;
return TRUE;
}
NTSTATUS
SetCacheAppData(
PSessCacheItem pItem,
PBYTE pbAppData,
DWORD cbAppData)
{
RtlAcquireResourceExclusive(&SchannelCache.Lock, TRUE);
if(pItem->pbAppData)
{
SPExternalFree(pItem->pbAppData);
}
pItem->pbAppData = pbAppData;
pItem->cbAppData = cbAppData;
RtlReleaseResource(&SchannelCache.Lock);
return STATUS_SUCCESS;
}
NTSTATUS
GetCacheAppData(
PSessCacheItem pItem,
PBYTE *ppbAppData,
DWORD *pcbAppData)
{
if(pItem->pbAppData == NULL)
{
*ppbAppData = NULL;
*pcbAppData = 0;
return STATUS_SUCCESS;
}
RtlAcquireResourceShared(&SchannelCache.Lock, TRUE);
*pcbAppData = pItem->cbAppData;
*ppbAppData = SPExternalAlloc(pItem->cbAppData);
if(*ppbAppData == NULL)
{
RtlReleaseResource(&SchannelCache.Lock);
return SP_LOG_RESULT(SEC_E_INSUFFICIENT_MEMORY);
}
memcpy(*ppbAppData, pItem->pbAppData, pItem->cbAppData);
RtlReleaseResource(&SchannelCache.Lock);
return STATUS_SUCCESS;
}
BOOL
IsEntryToBeProcessed(
PSessCacheItem pItem,
PLUID LogonID,
ULONG ProcessID,
LPWSTR pszTargetName,
DWORD dwFlags)
{
//
// Validate client entries.
//
if(pItem->fProtocol & SP_PROT_CLIENTS)
{
if((dwFlags & SSL_PURGE_CLIENT_ENTRIES) == 0 &&
(dwFlags & SSL_PURGE_CLIENT_ALL_ENTRIES) == 0)
{
return FALSE;
}
if((dwFlags & SSL_PURGE_CLIENT_ALL_ENTRIES) == 0)
{
if(!RtlEqualLuid(&pItem->LogonId, LogonID))
{
return FALSE;
}
}
if(pszTargetName != NULL)
{
if(pItem->szCacheID == NULL ||
wcscmp(pItem->szCacheID, pszTargetName) != 0)
{
return FALSE;
}
}
return TRUE;
}
//
// Validate server entries.
//
if(pItem->fProtocol & SP_PROT_SERVERS)
{
if((dwFlags & SSL_PURGE_SERVER_ENTRIES) == 0 &&
(dwFlags & SSL_PURGE_SERVER_ALL_ENTRIES) == 0)
{
return FALSE;
}
if(ProcessID != pItem->ProcessID)
{
if((dwFlags & SSL_PURGE_SERVER_ALL_ENTRIES) == 0)
{
return FALSE;
}
}
}
return TRUE;
}
NTSTATUS
SPCachePurgeEntries(
LUID *LogonID,
ULONG ProcessID,
LPWSTR pszTargetName,
DWORD dwFlags)
{
PSessCacheItem pItem;
PLIST_ENTRY pList;
LIST_ENTRY DeleteList;
DebugLog((DEB_TRACE, "Purge cache entries\n"));
//
// Initialize the list of deleted entries.
//
InitializeListHead(&DeleteList);
//
// Enumerate through the cache entries.
//
RtlAcquireResourceExclusive(&SchannelCache.Lock, TRUE);
pList = SchannelCache.EntryList.Flink;
while(pList != &SchannelCache.EntryList)
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, EntryList.Flink);
pList = pList->Flink;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
if(!IsEntryToBeProcessed(pItem,
LogonID,
ProcessID,
pszTargetName,
dwFlags))
{
continue;
}
if(pItem->cRef > 1)
{
// This entry is currently being used, so don't delete.
// Mark it as non-resumable, though.
pItem->ZombieJuju = FALSE;
pItem->DeferredJuju = FALSE;
continue;
}
if(pItem->ProcessID != 0 &&
pItem->ProcessID != ProcessID)
{
if(IsApplicationCertificateMapper(pItem->phMapper))
{
// This entry has a mapper structure that doesn't belong
// to the calling process, so don't delete. Mark it as
// non-resumable, though.
pItem->ZombieJuju = FALSE;
pItem->DeferredJuju = FALSE;
continue;
}
}
//
// Remove this entry from the cache, and add it to the list of
// entries to be destroyed.
//
RemoveEntryList(&pItem->IndexEntryList);
RemoveEntryList(&pItem->EntryList);
SchannelCache.dwUsedEntries--;
InsertTailList(&DeleteList, &pItem->EntryList);
}
RtlReleaseResource(&SchannelCache.Lock);
//
// Kill the purged zombies.
//
pList = DeleteList.Flink;
while(pList != &DeleteList)
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, EntryList.Flink);
pList = pList->Flink;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
SPCacheDelete(pItem);
}
return STATUS_SUCCESS;
}
NTSTATUS
SPCacheGetInfo(
LUID * LogonID,
LPWSTR pszTargetName,
DWORD dwFlags,
PSSL_SESSION_CACHE_INFO_RESPONSE pCacheInfo)
{
PSessCacheItem pItem;
PLIST_ENTRY pList;
DWORD timeNow;
ULONG ProcessID;
pCacheInfo->CacheSize = SchannelCache.dwMaximumEntries;
pCacheInfo->Entries = 0;
pCacheInfo->ActiveEntries = 0;
pCacheInfo->Zombies = 0;
pCacheInfo->ExpiredZombies = 0;
pCacheInfo->AbortedZombies = 0;
pCacheInfo->DeletedZombies = g_CacheCleanupCount;
timeNow = GetTickCount();
SslGetClientProcess(&ProcessID);
RtlAcquireResourceExclusive(&SchannelCache.Lock, TRUE);
pList = SchannelCache.EntryList.Flink;
while(pList != &SchannelCache.EntryList)
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, EntryList.Flink);
pList = pList->Flink;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
if(pItem->fProtocol & SP_PROT_CLIENTS)
{
if((dwFlags & SSL_RETRIEVE_CLIENT_ENTRIES) == 0)
{
continue;
}
}
else
{
if((dwFlags & SSL_RETRIEVE_SERVER_ENTRIES) == 0)
{
continue;
}
}
pCacheInfo->Entries++;
if(pItem->cRef == 1)
{
pCacheInfo->Zombies++;
if(HasTimeElapsed(pItem->CreationTime,
timeNow,
pItem->Lifespan))
{
pCacheInfo->ExpiredZombies++;
}
if(pItem->ZombieJuju == FALSE)
{
pCacheInfo->AbortedZombies++;
}
}
else
{
pCacheInfo->ActiveEntries++;
}
}
RtlReleaseResource(&SchannelCache.Lock);
return STATUS_SUCCESS;
}
NTSTATUS
SPCacheGetPerfmonInfo(
DWORD dwFlags,
PSSL_PERFMON_INFO_RESPONSE pPerfmonInfo)
{
PSessCacheItem pItem;
PLIST_ENTRY pList;
//
// Compute performance numbers.
//
pPerfmonInfo->ClientHandshakesPerSecond = g_cClientHandshakes;
pPerfmonInfo->ServerHandshakesPerSecond = g_cServerHandshakes;
pPerfmonInfo->ClientReconnectsPerSecond = g_cClientReconnects;
pPerfmonInfo->ServerReconnectsPerSecond = g_cServerReconnects;
//
// Compute cache info.
//
pPerfmonInfo->ClientCacheEntries = 0;
pPerfmonInfo->ServerCacheEntries = 0;
pPerfmonInfo->ClientActiveEntries = 0;
pPerfmonInfo->ServerActiveEntries = 0;
RtlAcquireResourceShared(&SchannelCache.Lock, TRUE);
pList = SchannelCache.EntryList.Flink;
while(pList != &SchannelCache.EntryList)
{
pItem = CONTAINING_RECORD(pList, SessCacheItem, EntryList.Flink);
pList = pList->Flink;
ASSERT(pItem->Magic == SP_CACHE_MAGIC);
if(pItem->fProtocol & SP_PROT_CLIENTS)
{
pPerfmonInfo->ClientCacheEntries++;
if(pItem->cRef > 1)
{
pPerfmonInfo->ClientActiveEntries++;
}
}
else
{
pPerfmonInfo->ServerCacheEntries++;
if(pItem->cRef > 1)
{
pPerfmonInfo->ServerActiveEntries++;
}
}
}
RtlReleaseResource(&SchannelCache.Lock);
return STATUS_SUCCESS;
}