Source code of Windows XP (NT5)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

726 lines
22 KiB

/*--
Copyright (c) 1995-1998 Microsoft Corporation
Module Name: ISAPI.CPP
Author: Arul Menezes
Abstract: ISAPI handling code
--*/
#include "pch.h"
#pragma hdrstop
#include "httpd.h"
#define RK_OLE L"SOFTWARE\\Microsoft\\Ole"
#define RK_FREETIMEOUT L"FreeTimeout"
// Amount of time ISAPI cache timeout thread should sleep between firing.
#define CACHE_SLEEP_TIMEOUT 60000
BOOL InitExtensions(CISAPICache **ppISAPICache, DWORD *pdwCacheSleep)
{
CReg reg(HKEY_LOCAL_MACHINE, RK_OLE);
// We double the amount of time COM tells us it's using to be safe.
*pdwCacheSleep = 3*reg.ValueDW(RK_FREETIMEOUT,600000); // default is 30 minutes, 1800000
*ppISAPICache = new CISAPICache();
return(NULL != *ppISAPICache);
}
BOOL CHttpRequest::ExecuteISAPI(void)
{
DWORD dwRet = HSE_STATUS_ERROR;
EXTENSION_CONTROL_BLOCK ECB;
CISAPI *pISAPIDLL = NULL;
// wrap all calls to the ISAPI in a _try--_except
__try
{
if (! g_pVars->m_pISAPICache->Load(m_wszPath,&pISAPIDLL))
return FALSE;
// create an ECB (this allocates no memory)
FillECB(&ECB);
DEBUGCHK(m_hEvent == NULL);
m_hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
m_pECB = &ECB;
// call the ISAPI
dwRet = pISAPIDLL->CallExtension(&ECB);
// grab log data if any
if (ECB.lpszLogData[0])
{
DEBUGCHK(!m_pszLogParam);
m_pszLogParam = MySzDupA(ECB.lpszLogData);
}
}
__except(1) // catch all exceptions
{
TraceTag(ttidWebServer, "ISAPI DLL caused exception 0x%08x and was terminated", GetExceptionCode());
g_pVars->m_pLog->WriteEvent(IDS_HTTPD_EXT_EXCEPTION,m_wszPath,GetExceptionCode(),L"HttpExtensionProc",GetLastError());
}
// set keep-alive status based on return code
m_fKeepAlive = (dwRet==HSE_STATUS_SUCCESS_AND_KEEP_CONN);
TraceTag(ttidWebServer, "ISAPI ExtProc returned %d keep=%d", dwRet, m_fKeepAlive);
if (dwRet == HSE_STATUS_PENDING)
{
DWORD dwRes = WAIT_ABANDONED;
// Wait up to 5 minutes for the request to complete
if (m_hEvent != NULL)
dwRes = WaitForSingleObject (m_hEvent, 300000);
AssertSz(dwRes == WAIT_OBJECT_0, "Pended request was timed out after 5 minutes");
if (dwRes == WAIT_OBJECT_0)
dwRet = m_dwStatus;
else
dwRet = HSE_STATUS_ERROR;
}
if (m_hEvent != NULL)
{
CloseHandle (m_hEvent);
m_hEvent = NULL;
}
StartRemoveISAPICacheIfNeeded();
g_pVars->m_pISAPICache->Unload(pISAPIDLL);
return(dwRet!=HSE_STATUS_ERROR);
}
void CHttpRequest::FillECB(LPEXTENSION_CONTROL_BLOCK pECB)
{
ZEROMEM(pECB);
pECB->cbSize = sizeof(*pECB);
pECB->dwVersion = HSE_VERSION;
pECB->ConnID = (HCONN)this;
DEBUGCHK(m_pszMethod);
// BUGBUG 13244: IIS examines dwHttpStatusCode if user doesn't send their own headers
// and uses it. However, there's a work around for this on CE (direct use to
// ServerSupportFunction) and using the status code rather than m_rs would
// be a rearchitecting problem. Fix: None for now.
pECB->dwHttpStatusCode = 200;
pECB->lpszMethod = m_pszMethod;
pECB->lpszQueryString = (PSTR)(m_pszQueryString ? m_pszQueryString : cszEmpty);
pECB->lpszContentType = (PSTR)(m_pszContentType ? m_pszContentType : cszEmpty);
pECB->lpszPathInfo = (PSTR) (m_pszPathInfo ? m_pszPathInfo : cszEmpty);
pECB->lpszPathTranslated = (PSTR) (m_pszPathTranslated ? m_pszPathTranslated : cszEmpty);
pECB->cbTotalBytes = m_dwContentLength;
pECB->cbAvailable = m_bufRequest.Count();
pECB->lpbData = m_bufRequest.Data();
pECB->GetServerVariable = ::GetServerVariable;
pECB->WriteClient = ::WriteClient;
pECB->ReadClient = ::ReadClient;
pECB->ServerSupportFunction = ::ServerSupportFunction;
}
BOOL WINAPI GetServerVariable(HCONN hConn, PSTR psz, PVOID pv, PDWORD pdw)
{
CHECKHCONN(hConn);
CHECKPTRS2(psz, pdw);
return((CHttpRequest*)hConn)->GetServerVariable(psz, pv, pdw, FALSE);
}
int RecvToBuf(SOCKET socket, PVOID pv, DWORD dwReadBufSize, DWORD dwTimeout)
{
DEBUG_CODE_INIT;
int iRecv = SOCKET_ERROR;
DWORD dwAvailable;
if (!MySelect(socket,dwTimeout))
{
SetLastError(WSAETIMEDOUT);
myleave(1400);
}
if (ioctlsocket(socket, FIONREAD, &dwAvailable))
{
SetLastError(WSAETIMEDOUT);
myleave(1401);
}
iRecv = recv(socket, (PSTR) pv, (dwAvailable < dwReadBufSize) ? dwAvailable : dwReadBufSize, 0);
if (iRecv == 0)
{
SetLastError(WSAETIMEDOUT);
myleave(1402);
}
if (iRecv == SOCKET_ERROR)
{
// recv() already has called SetLastError with appropriate message
myleave(1403);
}
done:
TraceTag(ttidWebServer, "RecvToBuf returns error err = %d, GLE = 0x%08x",err,GetLastError());
return iRecv;
}
BOOL WINAPI ReadClient(HCONN hConn, PVOID pv, PDWORD pdw)
{
CHECKHCONN(hConn);
CHECKPTRS2(pv, pdw);
if ( *pdw == 0)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
return((CHttpRequest*)hConn)->ReadClient(pv,pdw);
}
BOOL CHttpRequest::ReadClient(PVOID pv, PDWORD pdw)
{
DEBUG_CODE_INIT;
BOOL ret = FALSE;
PVOID pvFilterModify = pv;
DWORD dwBytesReceived;
DWORD dwBufferSize = *pdw;
dwBytesReceived = RecvToBuf(m_socket,pv,*pdw,RECVTIMEOUT);
if (dwBytesReceived == 0 || dwBytesReceived == SOCKET_ERROR)
myleave(1399);
if (g_pVars->m_fFilters &&
! CallFilter(SF_NOTIFY_READ_RAW_DATA,(PSTR*) &pvFilterModify,(int*) &dwBytesReceived, NULL, (int *) &dwBufferSize))
{
myleave(1404);
}
// Check if filter modified pointer, copy if there's enough room for it.
if (pvFilterModify != pv)
{
if (*pdw <= dwBufferSize)
{
memcpy(pv,pvFilterModify,dwBufferSize);
}
else
{
SetLastError(ERROR_INSUFFICIENT_BUFFER);
myleave(1405);
}
}
*pdw = dwBytesReceived;
ret = TRUE;
done:
TraceTag(ttidWebServer, "HTTPD:ReadClient failed, GLE = 0x%08x, err = % err",GetLastError(), err);
return ret;
}
BOOL WINAPI WriteClient(HCONN hConn, PVOID pv, PDWORD pdw, DWORD dwFlags)
{
CHECKHCONN(hConn);
CHECKPTRS2(pv, pdw);
if (dwFlags & HSE_IO_ASYNC)
return((CHttpRequest*)hConn)->WriteClientAsync(pv, pdw, FALSE);
else
return((CHttpRequest*)hConn)->WriteClient(pv, pdw,FALSE);
}
BOOL WINAPI ServerSupportFunction(HCONN hConn, DWORD dwReq, PVOID pvBuf, PDWORD pdwSize, PDWORD pdwType)
{
CHECKHCONN(hConn);
return((CHttpRequest*)hConn)->ServerSupportFunction(dwReq, pvBuf, pdwSize, pdwType);
}
BOOL CHttpRequest::ServerSupportFunction(DWORD dwReq, PVOID pvBuf, PDWORD pdwSize, PDWORD pdwType)
{
switch (dwReq)
{
// Can never support these
//case HSE_REQ_ABORTIVE_CLOSE:
//case HSE_REQ_ASYNC_READ_CLIENT:
//case HSE_REQ_GET_CERT_INFO_EX:
//case HSE_REQ_GET_IMPERSONATION_TOKEN:
//case HSE_REQ_GET_SSPI_INFO:
//case HSE_REQ_REFRESH_ISAPI_ACL:
//case HSE_REQ_TRANSMIT_FILE:
default:
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
case HSE_REQ_IS_KEEP_CONN:
{
CHECKPTR(pvBuf);
*((BOOL *) pvBuf) = m_fKeepAlive;
return TRUE;
}
case HSE_REQ_SEND_URL:
case HSE_REQ_SEND_URL_REDIRECT_RESP:
{
// close connection, because ISAPI won't have a chance to add headers anyway
CHttpResponse resp(m_socket, STATUS_MOVED, CONN_CLOSE,this);
// m_rs = STATUS_MOVED;
resp.SendRedirect((PSTR)pvBuf); // send a special redirect body
m_fKeepAlive = FALSE;
return TRUE;
}
case HSE_REQ_MAP_URL_TO_PATH_EX:
case HSE_REQ_MAP_URL_TO_PATH:
{
CHECKPTRS2(pvBuf,pdwSize);
if (dwReq == HSE_REQ_MAP_URL_TO_PATH_EX)
{
if (!pdwType)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
return MapURLToPath((PSTR)pvBuf,pdwSize,(LPHSE_URL_MAPEX_INFO) pdwType);
}
else
{
// IIS docs are misleading here, but even if a valid param is passed in non-EX
// case, ignore it. (Like IIS.)
return MapURLToPath((PSTR)pvBuf,pdwSize);
}
}
case HSE_REQ_SEND_RESPONSE_HEADER:
{
// no Connection header...let ISAPI send one if it wants
CHttpResponse resp(m_socket, STATUS_OK, CONN_NONE,this);
// no body, default or otherwise (leave that to the ISAPI), but add default headers
m_rs = STATUS_OK;
resp.SendResponse((PSTR) pdwType, (PSTR) pvBuf);
return TRUE;
}
case HSE_REQ_SEND_RESPONSE_HEADER_EX:
{
// Note: We ignore cchStatus and cchHeader members.
CHECKPTR(pvBuf);
HSE_SEND_HEADER_EX_INFO *pHeaderEx = (HSE_SEND_HEADER_EX_INFO *) pvBuf;
// Connection header determined by fKeepConn of passed in struct
CHttpResponse resp(m_socket, STATUS_OK,
pHeaderEx->fKeepConn ? CONN_KEEP : CONN_CLOSE,
this);
m_fKeepAlive = pHeaderEx->fKeepConn;
// no body, default or otherwise (leave that to the ISAPI), but add default headers
m_rs = STATUS_OK;
resp.SendResponse(pHeaderEx->pszHeader, pHeaderEx->pszStatus);
return TRUE;
}
case HSE_APPEND_LOG_PARAMETER:
{
return MyStrCatA(&m_pszLogParam,(PSTR) pvBuf,",");
}
case HSE_REQ_CLOSE_CONNECTION:
{
// Per ISAPI documentation:
//
// Once you use the HSE_REQ_CLOSE_CONNECTION server
// support function to close a connection, you must
// wait for IIS to call the asynchronous I/O function
// (specified by HSE_REQ_IO_COMPLETION) before you end
// the session with HSE_REQ_DONE_WITH_SESSION.
//
// Since our async I/O calls are all really sync, we
// will call the completion routine at the end of the
// operation and get a HSE_REQ_DONE_WITH_SESSION to do
// the rest of the cleanup.
return TRUE;
}
case HSE_REQ_DONE_WITH_SESSION:
{
AssertSz (m_hEvent != NULL, "Call to end a non-pended session, treating as error");
if (m_hEvent != NULL)
{
m_dwStatus = *((DWORD *) pvBuf);
m_fKeepAlive = FALSE;
SetEvent (m_hEvent);
return TRUE;
}
else
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
}
case HSE_REQ_IO_COMPLETION:
{
m_pfnCompletion = (PFN_HSE_IO_COMPLETION)pvBuf;
m_pvContext = (PVOID) pdwType;
return TRUE;
}
}
}
BOOL CHttpRequest::MapURLToPath(PSTR pszBuffer, PDWORD pdwSize, LPHSE_URL_MAPEX_INFO pUrlMapEx)
{
DWORD dwPermissions;
PWSTR wszPath;
PSTR pszURL;
DWORD dwBufNeeded = 0;
BOOL ret;
wszPath = g_pVars->m_pVroots->URLAtoPathW(pszBuffer,&dwPermissions);
if (!wszPath)
{
// Assume failure on matching to virtual root, and not on mem alloc
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
dwBufNeeded = (DWORD) WideCharToMultiByte(CP_ACP,0, wszPath, -1, pszBuffer, 0 ,0,0);
// For MAP_EX case, we set these vars from the passed structure, else we use the raw ptrs.
if (pUrlMapEx)
{
pszURL = pUrlMapEx->lpszPath;
*pdwSize = MAX_PATH;
}
else
{
pszURL = pszBuffer;
}
// To keep this like IIS, we translate "/" to "\". We do the conversion
// only if we're using filters or if there's enough space in the buffer.
if (g_pVars->m_fFilters || *pdwSize >= dwBufNeeded)
{
for (int i = 0; i < (int) wcslen(wszPath); i++)
{
if ( wszPath[i] == L'/')
wszPath[i] = L'\\';
}
}
if (FALSE == g_pVars->m_fFilters)
{
// We check to make sure buffer is the right size because WideToMultyByte
// will overwrite pieces of pszURL even on failure, leaving pszURL's
// content invalid
if (*pdwSize < dwBufNeeded)
{
SetLastError(ERROR_INSUFFICIENT_BUFFER);
ret = FALSE;
}
else
{
MyW2A(wszPath, pszURL, *pdwSize);
ret = TRUE;
}
*pdwSize = dwBufNeeded;
}
else
{
// for EX case, put original URL as optional 5th parameter.
ret = FilterMapURL(pszURL, wszPath, pdwSize,dwBufNeeded, pUrlMapEx ? pszBuffer : NULL);
}
if (ret && pUrlMapEx)
{
pUrlMapEx->cchMatchingPath = *pdwSize - 1; // don't count \0
pUrlMapEx->cchMatchingURL = strlen(pszBuffer);
pUrlMapEx->dwFlags = dwPermissions;
}
MyFree(wszPath);
return ret;
}
void CISAPI::Unload(PWSTR wszDLLName)
{
if (m_pfnTerminate)
{
__try
{
if (SCRIPT_TYPE_FILTER == m_scriptType)
((PFN_TERMINATEFILTER)m_pfnTerminate)(HSE_TERM_MUST_UNLOAD);
else if (SCRIPT_TYPE_EXTENSION == m_scriptType)
((PFN_TERMINATEEXTENSION)m_pfnTerminate)(HSE_TERM_MUST_UNLOAD);
else if (SCRIPT_TYPE_ASP == m_scriptType)
((PFN_TERMINATEASP)m_pfnTerminate)();
}
__except(1)
{
DWORD dwExceptionCode = SCRIPT_TYPE_FILTER == m_scriptType ?
IDS_HTTPD_FILT_EXCEPTION : IDS_HTTPD_EXT_EXCEPTION;
PWSTR wszFunction = SCRIPT_TYPE_FILTER == m_scriptType ?
L"TerminateFilter" : L"TerminateExtension";
TraceTag(ttidWebServer, "HTTPD: TerminateExtension faulted");
g_pVars->m_pLog->WriteEvent(dwExceptionCode,wszDLLName ? wszDLLName : m_wszDLLName,
GetExceptionCode(),wszFunction,GetLastError());
}
}
MyFreeLib(m_hinst);
m_hinst = NULL;
m_pfnTerminate = NULL;
}
BOOL CISAPI::Load(PWSTR wszPath)
{
m_hinst = LoadLibrary(wszPath);
if (!m_hinst)
return FALSE;
if (SCRIPT_TYPE_ASP == m_scriptType)
{
if (! (m_pfnHttpProc = GetProcAddress(m_hinst, CE_STRING("ExecuteASP"))))
goto error;
if (! (m_pfnTerminate = GetProcAddress(m_hinst, CE_STRING("TerminateASP"))))
goto error;
return TRUE;
}
if (SCRIPT_TYPE_EXTENSION == m_scriptType)
{
m_pfnGetVersion = GetProcAddress(m_hinst, CE_STRING("GetExtensionVersion"));
m_pfnHttpProc = GetProcAddress(m_hinst, CE_STRING("HttpExtensionProc"));
m_pfnTerminate = GetProcAddress(m_hinst, CE_STRING("TerminateExtension"));
}
else if (SCRIPT_TYPE_FILTER == m_scriptType)
{
m_pfnGetVersion = GetProcAddress(m_hinst, CE_STRING("GetFilterVersion"));
m_pfnHttpProc = GetProcAddress(m_hinst, CE_STRING("HttpFilterProc"));
m_pfnTerminate = GetProcAddress(m_hinst, CE_STRING("TerminateFilter"));
}
if (!m_pfnHttpProc || !m_pfnGetVersion)
goto error;
__try
{
// call GetVersion immediately after load on extensions and Filters, but not ASP
// if it's a filter we need to do some flags work first
if (SCRIPT_TYPE_FILTER == m_scriptType)
{
HTTP_FILTER_VERSION vFilt;
// IIS ignores ISAPI version info, so do we.
((PFN_GETFILTERVERSION)m_pfnGetVersion)(&vFilt);
dwFilterFlags = vFilt.dwFlags;
// client didn't set the prio flags, assign them to default
// If they set more than one prio we use the highest + ignore others
if (0 == (dwFilterFlags & SF_NOTIFY_ORDER_MASK))
{
dwFilterFlags |= SF_NOTIFY_ORDER_DEFAULT;
}
}
else if (SCRIPT_TYPE_EXTENSION == m_scriptType)
{
HSE_VERSION_INFO vExt;
// IIS ignores ISAPI version info, so do we.
((PFN_GETEXTENSIONVERSION)m_pfnGetVersion)(&vExt);
}
return TRUE;
}
__except(1)
{
DWORD dwExceptionCode = SCRIPT_TYPE_FILTER == m_scriptType ?
IDS_HTTPD_FILT_EXCEPTION : IDS_HTTPD_EXT_EXCEPTION;
PWSTR wszFunction = SCRIPT_TYPE_FILTER == m_scriptType ?
L"GetFilterVersion" : L"GetExtensionVersion";
TraceTag(ttidWebServer, "HTTPD: GetExtensionVersion faulted");
g_pVars->m_pLog->WriteEvent(dwExceptionCode,wszPath,GetExceptionCode(),wszFunction,GetLastError());
}
// fall through to error
error:
m_pfnGetVersion = 0;
m_pfnHttpProc = 0;
m_pfnTerminate = 0;
MyFreeLib(m_hinst);
m_hinst = 0;
return FALSE;
}
//**********************************************************************
// ISAPI Caching functions
//**********************************************************************
HINSTANCE CISAPICache::Load(PWSTR wszDLLName, CISAPI **ppISAPI, SCRIPT_TYPE st)
{
DEBUG_CODE_INIT;
BOOL ret = FALSE;
PISAPINODE pTrav = NULL;
EnterCriticalSection(&m_CritSec);
for (pTrav = m_pHead; pTrav != NULL; pTrav = pTrav->m_pNext)
{
if ( 0 == lstrcmpi(pTrav->m_pISAPI->m_wszDLLName, wszDLLName))
{
TraceTag(ttidWebServer, "Found ISAPI dll in cache, name = %s, cur ref count = %d",
wszDLLName, pTrav->m_pISAPI->m_cRef);
break;
}
}
if (NULL == pTrav)
{
TraceTag(ttidWebServer, "ISAPI dll name = %s not found in cache, creating new entry",wszDLLName);
if (NULL == (pTrav = MyAllocNZ(ISAPINODE)))
myleave(1200);
if (NULL == (pTrav->m_pISAPI = new CISAPI(st)))
myleave(1201);
if (NULL == (pTrav->m_pISAPI->m_wszDLLName = MySzDupW(wszDLLName)))
myleave(1202);
if (! pTrav->m_pISAPI->Load(wszDLLName))
myleave(1203);
pTrav->m_pNext = m_pHead;
m_pHead = pTrav;
}
pTrav->m_pISAPI->m_cRef++;
*ppISAPI = pTrav->m_pISAPI;
ret = TRUE;
done:
if (!ret)
{
if (pTrav && pTrav->m_pISAPI)
{
MyFree(pTrav->m_pISAPI->m_wszDLLName);
delete pTrav->m_pISAPI;
}
MyFree(pTrav);
}
TraceTag(ttidWebServer, "CISAPICache::LoadISAPI failed, err = %d, GLE = 0x%08x",err,GetLastError());
LeaveCriticalSection(&m_CritSec);
if (ret)
return pTrav->m_pISAPI->m_hinst;
return NULL;
}
void CHttpRequest::StartRemoveISAPICacheIfNeeded()
{
DEBUGCHK(g_pVars->m_fExtensions);
if (InterlockedCompareExchange(&g_pVars->m_fISAPICacheRunning,1,0) == 0)
{
TraceTag(ttidWebServer, "HTTPD: ExecuteISAPI: Creating RemoveUnusedISAPIs timer");
// We never stop the timer. This is handled on call to Thread Pool Shutdown
g_pVars->m_pThreadPool->StartTimer(RemoveUnusedISAPIs,0,CACHE_SLEEP_TIMEOUT);
}
}
// lpv = 0 for thread that sleeps forever.
// lpv = 1 to remove all ISAPIs, called on shutdown.
DWORD WINAPI RemoveUnusedISAPIs(LPVOID lpv)
{
// We set lpv to 1 if we want to unload all ISAPIs during shutdown.
if (lpv)
{
g_pVars->m_pISAPICache->RemoveUnusedISAPIs(TRUE);
return 0;
}
if (g_pVars->m_pISAPICache->m_pHead != NULL)
g_pVars->m_pISAPICache->RemoveUnusedISAPIs(FALSE);
g_pVars->m_pThreadPool->StartTimer(RemoveUnusedISAPIs,0,CACHE_SLEEP_TIMEOUT);
return 0;
}
// to milliseconds (10 * 1000)
#define FILETIME_TO_MILLISECONDS ((__int64)10000L)
// fRemoveAll = TRUE ==> remove all ISAPI's, we're shutting down
// = FALSE ==> only remove ones who aren't in use and whose time has expired
void CISAPICache::RemoveUnusedISAPIs(BOOL fRemoveAll)
{
PISAPINODE pTrav = NULL;
PISAPINODE pFollow = NULL;
PISAPINODE pDelete = NULL;
SYSTEMTIME st;
__int64 ft;
EnterCriticalSection(&m_CritSec);
GetSystemTime(&st);
SystemTimeToFileTime(&st,(FILETIME*) &ft);
// Figure out what time it was g_pVars->m_dwCacheSleep milliseconds ago.
// Elements that haven't been used since then and that have no references
// are deleted.
ft -= FILETIME_TO_MILLISECONDS * g_pVars->m_dwCacheSleep;
for (pTrav = m_pHead; pTrav != NULL; )
{
if (fRemoveAll ||
(pTrav->m_pISAPI->m_cRef == 0 && pTrav->m_pISAPI->m_ftLastUsed < ft))
{
TraceTag(ttidWebServer, "Freeing unused ISAPI Dll %s",pTrav->m_pISAPI->m_wszDLLName);
pTrav->m_pISAPI->Unload();
delete pTrav->m_pISAPI;
if (pFollow)
pFollow->m_pNext = pTrav->m_pNext;
else
m_pHead = pTrav->m_pNext;
pDelete = pTrav;
pTrav = pTrav->m_pNext;
MyFree(pDelete);
}
else
{
if (pTrav->m_pISAPI->m_cRef == 0)
pTrav->m_pISAPI->CoFreeUnusedLibrariesIfASP();
pFollow = pTrav;
pTrav = pTrav->m_pNext;
}
}
LeaveCriticalSection(&m_CritSec);
}