|
|
/**********************************************************************/ /** Microsoft Windows NT **/ /** Copyright(c) Microsoft Corp., 1994 **/ /**********************************************************************/
/*
conn.cxx
This module contains the connection class
FILE HISTORY: Johnl 15-Aug-1994 Created
*/
#include "w3p.hxx"
#include <issched.hxx>
#pragma warning( disable:4355 ) // 'this' used in base member initializer list
//
// Used for iisprobe
//
extern "C" { VOID DumpW3InfoToHTML( OUT CHAR * pch, IN OUT LPDWORD pcb ); };
//
// Globals
//
#if CC_REF_TRACKING
//
// Ref count trace log size
//
#define C_CLIENT_CONN_REFTRACES 4000
#define C_LOCAL_CONN_REFTRACES 40
#endif
//
// Ref trace log for CLIENT_CONN objects
// NOTE we make this global so other classes can get at it
//
#if DBG
PTRACE_LOG g_pDbgCCRefTraceLog = NULL; #endif
//
// Private constants.
//
//
// How often to run the free list scavenger (thirty minutes)
//
#define FREE_LIST_SCAVENGE_TIME (30 * 60 * 1000)
//
// Private globals.
//
CRITICAL_SECTION CLIENT_CONN::_csBuffList; LIST_ENTRY CLIENT_CONN::_BuffListHead; BOOL CLIENT_CONN::_fGlobalInit = FALSE; DWORD CLIENT_CONN::_cFree = 0; // Number of items on lookaside free list
DWORD CLIENT_CONN::_FreeListScavengerCookie;
DWORD ErrorRespTable[] = {IDS_EXECUTE_ACCESS_DENIED, IDS_READ_ACCESS_DENIED, IDS_WRITE_ACCESS_DENIED, IDS_SSL_REQUIRED, IDS_SSL128_REQUIRED, IDS_ADDR_REJECT, IDS_CERT_REQUIRED, IDS_SITE_ACCESS_DENIED, IDS_TOO_MANY_USERS, IDS_INVALID_CNFG, IDS_PWD_CHANGE, IDS_MAPPER_DENY_ACCESS, #if defined(CAL_ENABLED)
IDS_CAL_EXCEEDED, #endif
IDS_CERT_REVOKED, IDS_DIR_LIST_DENIED, IDS_SITE_RESOURCE_BLOCKED, IDS_CERT_BAD, IDS_CERT_TIME_INVALID, IDS_SITE_NOT_FOUND };
DWORD SubStatusTable[] = {MD_ERROR_SUB403_EXECUTE_ACCESS_DENIED, MD_ERROR_SUB403_READ_ACCESS_DENIED , MD_ERROR_SUB403_WRITE_ACCESS_DENIED , MD_ERROR_SUB403_SSL_REQUIRED , MD_ERROR_SUB403_SSL128_REQUIRED , MD_ERROR_SUB403_ADDR_REJECT , MD_ERROR_SUB403_CERT_REQUIRED , MD_ERROR_SUB403_SITE_ACCESS_DENIED , MD_ERROR_SUB403_TOO_MANY_USERS , MD_ERROR_SUB403_INVALID_CNFG , MD_ERROR_SUB403_PWD_CHANGE , MD_ERROR_SUB403_MAPPER_DENY_ACCESS , #if defined(CAL_ENABLED)
MD_ERROR_SUB403_CAL_EXCEEDED , #endif
MD_ERROR_SUB403_CERT_REVOKED, MD_ERROR_SUB403_DIR_LIST_DENIED, MD_ERROR_SUB503_CPU_LIMIT, MD_ERROR_SUB403_CERT_BAD, MD_ERROR_SUB403_CERT_TIME_INVALID, MD_ERROR_SUB404_SITE_NOT_FOUND };
/*******************************************************************
Maco support for CLIENT_CONN::Reference/Dereference
HISTORY: DaveK 10-Sep-1997 Added ref trace logging
********************************************************************/
#if CC_REF_TRACKING
#define CC_LOG_REF_COUNT( cRefs ) \
\ SHARED_LOG_REF_COUNT( \ cRefs \ , (PVOID) this \ , _phttpReq \ , (_phttpReq) \ ? ((HTTP_REQUEST *) _phttpReq)->QueryWamRequest() \ : NULL \ , (_phttpReq) \ ? _phttpReq->QueryState() \ : NULL \ ); \ LOCAL_LOG_REF_COUNT( \ cRefs \ , (PVOID) this \ , _phttpReq \ , (_phttpReq) \ ? ((HTTP_REQUEST *) _phttpReq)->QueryWamRequest() \ : NULL \ , (_phttpReq) \ ? _phttpReq->QueryState() \ : NULL \ ); #else
#define CC_LOG_REF_COUNT( cRefs )
#endif
#if CC_REF_TRACKING
//
// ATQ notification trace
//
// ATQ notification : stored with context1 set magic value fefefefe
//
#define CCA_LOG_REF_COUNT( cRefs,BytesWritten,CompletionStatus, dwSig) \
\ SHARED_LOG_REF_COUNT( \ cRefs \ , (PVOID) this \ , (LPVOID)dwSig \ , (LPVOID)BytesWritten \ , CompletionStatus \ ); \ LOCAL_LOG_REF_COUNT( \ cRefs \ , (PVOID) this \ , (LPVOID)dwSig \ , (LPVOID)BytesWritten \ , CompletionStatus \ ); #else
#define CCA_LOG_REF_COUNT( Ctx,BytesWritten,CompletionStatus, dwSig)
#endif
//
// Private prototypes.
//
VOID WINAPI ClientConnTrimScavenger( PVOID pContext );
inline BOOL FastScanForTerminator( const CHAR * pch, UINT cbData ) /*++
Routine Description:
Check if buffer contains a full HTTP header. Can return false negatives.
Arguments:
pch - request buffer cbData - # of bytes in pch, excluding trailing '\0'
Return Value:
TRUE if buffer contains a full HTTP header FALSE if could not insure this, does not mean there is no full HTTP header.
--*/ { return ( (cbData > 4) && (!memcmp(pch+cbData - sizeof("\r\n\r\n") + 1, "\r\n\r\n", sizeof("\r\n\r\n") - 1 ) || !memcmp(pch+cbData - sizeof("\n\n") + 1, "\n\n", sizeof("\n\n")-1 ) ) ); } // FastScanForTerminator()
//
// Public functions.
//
BYTE * ScanForTerminator( const TCHAR * pch ) /*++
Routine Description:
Returns the first byte of data after the header
Arguments:
pch - Zero terminated buffer
Return Value:
Pointer to first byte of data after the header or NULL if the header isn't terminated
--*/ { while ( *pch ) { if ( !(pch = strchr( pch, '\n' ))) { break; }
//
// If we find an EOL, check if the next character is an EOL character
//
// NYI: UGLY cast is used ...
if ( *(pch = SkipWhite( (char * ) (pch + 1) )) == W3_EOL ) { return (BYTE *) pch + 1; } else if ( *pch ) { pch++; } }
return NULL; }
//
// Private functions.
//
/*******************************************************************
NAME: CLIENT_CONN
SYNOPSIS: Constructor for client connection
ENTRY: sClient - Client socket psockaddrLocal - Optional Addressing info of server socket psockaddrRemote - Addressing info for client patqContext - Optional ATQ context
HISTORY: Johnl 15-Aug-1994 Created
********************************************************************/
CLIENT_CONN::CLIENT_CONN( IN PCLIENT_CONN_PARAMS ClientParams ) : _phttpReq ( NULL), m_pInstance ( NULL), _fIsValid ( FALSE ) { #if CC_REF_TRACKING
_pDbgCCRefTraceLog = CreateRefTraceLog( C_LOCAL_CONN_REFTRACES, 0 ); #endif
Initialize( ClientParams );
//
// initialize statistics pointer to point to global statistics;
// it will point to local copy of the instance when instance
// pointer is set
//
m_pW3Stats = g_pW3Stats; }
VOID CLIENT_CONN::Initialize( IN PCLIENT_CONN_PARAMS ClientParams ) /*++
Routine Description:
This is a pseudo constructor, called just before this object is given to somebody who allocated a new client connection
Arguments:
sClient - Same as for constructor psockaddr - Same as for constructor
--*/ { CHAR * pchAddr; PSOCKADDR pAddrLocal = ClientParams->pAddrLocal;
_Signature = CLIENT_CONN_SIGNATURE; _sClient = ClientParams->sClient; _ccState = CCS_STARTUP; _cRef = 1; _AtqContext = ClientParams->pAtqContext; _fIsValid = FALSE; _fReuseContext = TRUE; _fAbortiveClose = FALSE; m_atqEndpointObject = ClientParams->pEndpointObject;
//
// Get the endpoint and reference it
//
m_pW3Endpoint = ClientParams->pEndpoint; //
// Don't actually reference the endpoint. The CLIENT_CONN reference is meaningless
//
// m_pW3Endpoint->Reference( );
_fSecurePort = m_pW3Endpoint->IsSecure( );
_pvInitial = ClientParams->pvInitialBuff; _cbInitial = ClientParams->cbInitialBuff;
g_pW3Stats->IncrCurrentConnections();
IF_DEBUG( CONNECTION ) { DBGPRINTF((DBG_CONTEXT, "Initializing connection object %lx, new user count %d\n", this, g_pW3Stats->QueryCurrentConnections() )); }
LockGlobals(); InsertHeadList( &listConnections, &ListEntry ); UnlockGlobals();
m_remoteIpAddress = ((PSOCKADDR_IN)ClientParams->pAddrRemote)->sin_addr.s_addr;
_sRemotePort = ntohs( ((PSOCKADDR_IN)ClientParams->pAddrRemote)->sin_port );
DBG_ASSERT( ClientParams->pAddrRemote->sa_family == AF_INET );
InetNtoa( ((SOCKADDR_IN *)ClientParams->pAddrRemote)->sin_addr, _achRemoteAddr ); _acCheck.BindAddr( ClientParams->pAddrRemote );
//
// look at local address
//
if (pAddrLocal->sa_family == AF_INET) { InetNtoa( ((SOCKADDR_IN *)pAddrLocal)->sin_addr, _achLocalAddr ); _sPort = m_pW3Endpoint->QueryPort( );
m_localIpAddress = ((PSOCKADDR_IN)ClientParams->pAddrLocal)->sin_addr.s_addr;
DBG_ASSERT(_sPort == ntohs( ((SOCKADDR_IN *)pAddrLocal)->sin_port)); DBG_ASSERT(_sPort != 0); } else { //
// This should not happen
// Remote winsock bug returns all zeros
//
DBGPRINTF((DBG_CONTEXT, "Invalid socket family %d\n",pAddrLocal->sa_family));
SetLastError( ERROR_INVALID_PARAMETER ); goto error; }
DBG_ASSERT( (strlen( _achLocalAddr ) + 1) <= sizeof(_achLocalAddr));
if ( _phttpReq != NULL ) { _phttpReq->InitializeSession( this, _pvInitial, _cbInitial ); } else { _phttpReq = new HTTP_REQUEST( this, _pvInitial, _cbInitial );
if ( (_phttpReq == NULL) || !_phttpReq->IsValid() ) { DBGPRINTF((DBG_CONTEXT,"Cannot allocate HTTP_REQUEST object\n"));
if (_phttpReq != NULL) { delete _phttpReq; _phttpReq = NULL; } SetLastError( ERROR_NOT_ENOUGH_MEMORY ); goto error; } }
CC_LOG_REF_COUNT( _cRef );
_fIsValid = TRUE; return;
error:
CC_LOG_REF_COUNT( _cRef );
//
// Set instance to null so that it will not be dereferenced
// twice (one on failure, one on cleanup)
//
//
// Set AtqContext to null so that it will not be dereferenced
// twice (one on failure, one on cleanup)
// it's like deja vu all over again.
_AtqContext = NULL;
return;
} // CLIENT_CONN::Initialize()
CLIENT_CONN::~CLIENT_CONN() { if (_phttpReq != NULL) { delete _phttpReq; }
#if CC_REF_TRACKING
DestroyRefTraceLog( _pDbgCCRefTraceLog ); #endif
_Signature = CLIENT_CONN_SIGNATURE_FREE; }
VOID CLIENT_CONN::Reset( VOID ) /*++
Routine Description:
This is a pseudo destructor, called just before this object is put back onto the free list.
Arguments:
--*/ {
//
// before we reset this client-conn:
// we assert that httpreq is null (we may have reached here
// on a failure path) or that our httpreq was unbound from its wamreq
//
DBG_ASSERT( ( _phttpReq == NULL ) || ( ((HTTP_REQUEST *)_phttpReq)->QueryWamRequest() == NULL ) );
IF_DEBUG( CONNECTION ) { DBGPRINTF((DBG_CONTEXT, "Resetting connection object %lx, AtqCont = %lx new user count %d\n", this, _AtqContext, g_pW3Stats->QueryCurrentConnections() )); }
if ( _phttpReq != NULL ) { _phttpReq->SessionTerminated(); }
if ( QueryAtqContext() != NULL ) { AtqFreeContext( QueryAtqContext(), _fReuseContext ); _AtqContext = NULL; }
_acCheck.UnbindAddr();
//
// Dereference the instance
//
if ( m_pInstance != NULL ) { m_pInstance->DecrementCurrentConnections(); m_pInstance->Dereference( ); m_pInstance = NULL;
//
// Reset statistics pointer to point to global statistics
// as the statistic object is associated with the instance.
//
m_pW3Stats = g_pW3Stats; }
//
// Dereference the endpoint
//
if ( m_pW3Endpoint != NULL ) { //
// Don't actually dereference. The CLIENT_CONN references are meaningless anyway.
//
// m_pW3Endpoint->Dereference( );
m_pW3Endpoint = NULL; }
//
// Remove ourselves from the connection list and knock down our
// connected user count
//
LockGlobals(); RemoveEntryList( &ListEntry ); DBG_ASSERT( ((LONG ) g_pW3Stats->QueryCurrentConnections()) >= 0 ); UnlockGlobals();
g_pW3Stats->DecrCurrentConnections();
_Signature = CLIENT_CONN_SIGNATURE_FREE;
} // CLIENT_CONN::Reset
/*******************************************************************
NAME: CLIENT_CONN::DoWork
SYNOPSIS: Worker method driven by thread pool queue
RETURNS: TRUE while processing should continue on this object If FALSE is returned, then the object should be deleted (status codes will already have been communicated to the client).
NOTES: If an IO request completes with an error, the connection is immediately closed and everything is cleaned up. The worker functions will not be called when an error occurs.
HISTORY: Johnl 15-Aug-1994 Created
********************************************************************/
BOOL CLIENT_CONN::DoWork( DWORD BytesWritten, DWORD CompletionStatus, BOOL fIOCompletion ) { BOOL fRet = TRUE; BOOL fFinished = FALSE; BOOL fAvailableData; BOOL fDoAgain;
IF_DEBUG( CONNECTION ) { DBGPRINTF(( DBG_CONTEXT, "DoWork: Object %lx Last IO error %lu Bytes = %d State = %d\n" "\tIsIO = %s Ref = %d Sock = %p\n", this, CompletionStatus, BytesWritten, QueryState(), fIOCompletion ? "TRUE" : "FALSE", QueryRefCount(), (QueryAtqContext() ? QueryAtqContext()->hAsyncIO : (PVOID)-1L) )); }
//
// If this was a completion generated by an IO request, decrement the
// ref count
//
if ( fIOCompletion ) { // NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
Dereference(); }
//
// If an IO Request completed with an error and we're not already in the
// process of cleaning up, then abort the connection and cleanup. We
// do not send a status code to the client in this instance.
//
if (_phttpReq == NULL) { return FALSE; }
_phttpReq->SetLastCompletionStatus( BytesWritten, CompletionStatus );
if ( CompletionStatus && !IsCleaningUp() ) { //
// We want to log this error iff we've received some bytes
// on the connection already. Otherwise this could be a
// 'natural' abort after a successful request.
//
if ( _phttpReq->QueryBytesReceived() != 0) { //Log status may still be set to HT_DONT_LOG; if it is, change it
//to "bad request" because we always want to log errors.
DWORD dwLogHttpResponse = _phttpReq->QueryLogHttpResponse(); _phttpReq->SetLogStatus ( ( dwLogHttpResponse == HT_DONT_LOG ? HT_BAD_REQUEST : dwLogHttpResponse ), CompletionStatus ); }
#if DBG
if ( CompletionStatus != ERROR_NETNAME_DELETED && CompletionStatus != ERROR_OPERATION_ABORTED ) { DBGPRINTF(( DBG_CONTEXT, "DoWork: Aborting client connection %8lx, error %d\n", this, CompletionStatus )); } #endif
if ( HTR_GATEWAY_ASYNC_IO == _phttpReq->QueryState()) {
DBGPRINTF(( DBG_CONTEXT , "CLIENT_CONN[%08x]::DoWork async i/o failed " "_phttpReq[%08x] " "pWamRequest[%08x] " "CompletionStatus(%d) " "\n" , this , _phttpReq , ((HTTP_REQUEST * )_phttpReq)->QueryWamRequest() , CompletionStatus ));
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
// Notify the external gateway application
// NYI: This is a hack for IIS 2.0 We need to fix state machines
// of CLIENT_CONN & HTTP_REQUEST for future extensions
((HTTP_REQUEST * )_phttpReq)->ProcessAsyncGatewayIO(); } else {
//
// if this is not a WAM request, cleanup Exec, etc...
//
_phttpReq->WriteLogRecord();
}
Disconnect( NULL, 0, 0, FALSE );
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
goto Exit; }
switch ( QueryState() ) { case CCS_STARTUP:
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
//
// Do this at the beginning of every request
//
fRet = OnSessionStartup( &fDoAgain, _pvInitial, _cbInitial, TRUE);
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
if ( !fRet || !_pvInitial ) { break; }
//
// Fall through
//
case CCS_PROCESSING_CLIENT_REQ:
//
// Set the start time on this request
//
_phttpReq->SetRequestStartTime();
do {
fDoAgain = FALSE;
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
fRet = _phttpReq->DoWork( &fFinished );
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
if ( !fRet ) { //
// If we were denied access to the resource, then ask the user
// for better authorization. Unless the user is in the process
// of authenticating, we force a disconnect. This prevents the
// case of logging on as anonymous successfully and then failing
// to access the resource.
//
if ( GetLastError() == ERROR_ACCESS_DENIED ) { if ( !_phttpReq->IsAuthenticating() ) { _phttpReq->SetKeepConn( FALSE ); }
if ( _phttpReq->IsKeepConnSet() ) { //
// Zero out the inital buffer so we don't try and reuse
// it next time around
//
_pvInitial = NULL; _cbInitial = 0;
SetState( CCS_STARTUP ); } else { _phttpReq->SetLogStatus( HT_DENIED, ERROR_ACCESS_DENIED ); SetState( CCS_DISCONNECTING ); }
fRet = _phttpReq->SendAuthNeededResp( &fFinished );
if ( fRet && fFinished ) { goto Disconnecting; } } } else if ( fFinished ) { _phttpReq->WriteLogRecord();
if ( !IsCleaningUp() ) { if ( !_phttpReq->IsKeepConnSet() || !(fRet = OnSessionStartup(&fDoAgain)) ) { //
// Either we completed the disconnect so
// close the socket or an error
// occurred setting up for the next request w/ KeepConn set
//
Disconnect(); fDoAgain = FALSE; }
fFinished = FALSE; } }
} while ( fDoAgain );
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
break;
case CCS_DISCONNECTING:
Disconnecting: //
// Write the log record for this request
//
_phttpReq->WriteLogRecord();
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
Disconnect();
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
//
// Fall-through to async i/o cleanup code ...
//
case CCS_SHUTDOWN:
//
// If we are still in async i/o state when shutting down, cancel
//
// NOTE this should only happen in cases like 97842,
// where oop isapi submited async i/o and then crashed
//
if ( _phttpReq->QueryState() == HTR_GATEWAY_ASYNC_IO ) {
DBGPRINTF(( DBG_CONTEXT , "CLIENT_CONN::DoWork: calling CancelAsyncGatewayIO " "State = %d, this = %lx, Ref = %d\n" , QueryState() , this , QueryRefCount() ));
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
((HTTP_REQUEST * )_phttpReq)->CancelAsyncGatewayIO();
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
}
break;
default: fRet = FALSE; DBG_ASSERT( FALSE ); }
//
// If an error occurred, disconnect without sending a response
//
if ( !fRet ) {
//
// There was a problem with SetLogStatus blowing away the
// last error, so save it here and assert that it does not
// get changed.
//
DWORD dwTempError = GetLastError();
_phttpReq->SetLogStatus( HT_SERVER_ERROR, dwTempError );
_phttpReq->WriteLogRecord();
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
_phttpReq->Disconnect( HT_SERVER_ERROR, dwTempError );
// NOTE negative ref count ==> no change to ref count
CC_LOG_REF_COUNT( -_cRef );
}
Exit: IF_DEBUG( CONNECTION ) { DBGPRINTF((DBG_CONTEXT, "DoWork: Leaving, State = %d, this = %lx, Ref = %d\n", QueryState(), this, QueryRefCount() )); }
//
// This connection's reference count should always be at least one
// at this point
//
DBG_ASSERT( QueryRefCount() > 0 );
return TRUE;
} // CLIENT_CONN::DoWork
/*******************************************************************
NAME: CLIENT_CONN::OnSessionStartup
SYNOPSIS: Initiates the first read to get the client request
PARAMETERS:
RETURNS: TRUE if processing should continue, FALSE to abort the this connection
HISTORY: Johnl 15-Aug-1994 Created
********************************************************************/
BOOL CLIENT_CONN::OnSessionStartup( BOOL *pfDoAgain, PVOID pvInitial, DWORD cbInitial, BOOL fFirst ) { APIERR err = NO_ERROR;
//
// Associate our client socket with Atq if it hasn't already
//
if ( !QueryAtqContext() ) { DBG_ASSERT( pvInitial == NULL ); if ( !AtqAddAsyncHandle( &_AtqContext, m_atqEndpointObject, this, W3Completion, W3_DEF_CONNECTION_TIMEOUT, (HANDLE) QuerySocket())) { DBGPRINTF(( DBG_CONTEXT, "OnSessionStartup: failed to add Atq handle, error %lu\n", GetLastError() ));
return FALSE; } }
SetState( CCS_PROCESSING_CLIENT_REQ );
return _phttpReq->StartNewRequest( pvInitial, cbInitial, fFirst, pfDoAgain); }
DWORD ErrorResponseToSubStatus( DWORD dwErrorResponse ) /*++
Routine Description:
Map an 'ErrorResponse' to a substatus for use in custom error lookup.
Arguments:
dwErrorResponse - The error response to be mapped.
Return Value:
The substatus if we can map it, or 0 otherwise.
--*/ { int i;
DBG_ASSERT((sizeof(ErrorRespTable)/sizeof(DWORD)) == (sizeof(SubStatusTable)/sizeof(DWORD)));
for (i = 0; i < sizeof(ErrorRespTable)/sizeof(DWORD);i++) { if (ErrorRespTable[i] == dwErrorResponse) { return SubStatusTable[i]; } }
return 0; }
#define ERROR_HTML_PREFIX "<head><title>Error</title></head><body>"
/*******************************************************************
NAME: CLIENT_CONN::Disconnect
SYNOPSIS: Initiates a disconnect from the client
ENTRY: pRequest - If not NULL and HTResponse is non-zero, send a response status before disconnecting HTResponse - HTTP status code to send ErrorResponse - Optional information string (system error or string resource ID).
NOTES: If a response is sent, then the socket won't be disconnected till the send completes (state goes to
HISTORY: Johnl 22-Aug-1994 Created
********************************************************************/
VOID CLIENT_CONN::Disconnect( HTTP_REQ_BASE * pRequest, DWORD HTResponse, DWORD ErrorResponse, BOOL fDoShutdown, LPBOOL pfFinished ) { CHAR *pszResp; BOOL fDone; DWORD dwSubStatus = 0; STACK_STR(strError, 80); STACK_STR(strResp, 80); CHAR ach[17];
//
// If Disconnect has already been called, then this is a no-op
//
if ( QueryState() == CCS_SHUTDOWN ) { return; }
if ( _fAbortiveClose ) { fDoShutdown = FALSE; }
if ( pRequest && HTResponse ) { STACK_STR(strBody, 200); BOOL bBodyBuilt; DWORD dwRespLength; DWORD dwContentLength;
if ( HTResponse == HT_NOT_FOUND ) { QueryW3StatsObj()->IncrTotalNotFoundErrors(); }
//
// Means we have to wait for the status response before closing
// the socket
//
SetState( CCS_DISCONNECTING );
IF_DEBUG( CONNECTION ) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Going to Disconnecting for %lx, ref = %d, response = %d\n", this, QueryRefCount(), HTResponse )); }
if ( ErrorResponse != 0) { dwSubStatus = ErrorResponseToSubStatus(ErrorResponse); }
if (pRequest->CheckCustomError(&strError, HTResponse, dwSubStatus, &fDone, &dwContentLength)) { // Had at least some custom error processing. If we're done, just return.
if (fDone) { return; }
// Otherwise we've got a custom error body. Build up the basic
// status line that we need, convert the length
// to ascii for use as a Content-Length, build up what we need in
// strBody, and keep going.
if ( !HTTP_REQ_BASE::BuildStatusLine( &strResp, HTResponse, 0, pRequest->QueryURL(), NULL)) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Failed to send status (error %d), aborting connecting\n", ::GetLastError()));
goto DisconnectNow; }
_itoa( dwContentLength, ach, 10 );
bBodyBuilt = strBody.Copy("Content-Length: ", sizeof("Content-Length: ") - 1) && strBody.Append(ach) && strBody.Append("\r\n", sizeof("\r\n") - 1) && strBody.Append((CHAR *)strError.QueryPtr());
strResp.SetLen(strlen((CHAR *)strResp.QueryPtr()));
} else { //
// No custom error, build the standard status line and body.
//
// Send the requested status response and after that completes close
// the socket
//
if (ErrorResponse != 0 && ErrorResponse < STR_RES_ID_BASE) { if (!strError.Copy(ERROR_HTML_PREFIX, sizeof(ERROR_HTML_PREFIX) - 1)) { goto DisconnectNow; } }
if ( !HTTP_REQ_BASE::BuildStatusLine( &strResp, HTResponse, ErrorResponse, pRequest->QueryURL(), &strError)) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Failed to send status (error %d), aborting connecting\n", ::GetLastError()));
goto DisconnectNow; }
//
// Now build up the body.
strResp.SetLen(strlen((CHAR *)strResp.QueryPtr()));
dwRespLength = strResp.QueryCB() - CRLF_SIZE;
if (!strBody.Copy("Content-Type: text/html\r\n", sizeof("Content-Type: text/html\r\n") - 1)) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Failed to build error message for error %d, aborting connecting\n", HTResponse)); goto DisconnectNow; }
// The body we'll build depends on whether or not there's an
// error string. If there is, then that's it, we'll just copy it
// in. If not, we'll fabricate a short HTML document from the
// status line.
if (strError.IsEmpty()) { // No error string.
dwContentLength = (dwRespLength * 2) - (sizeof("HTTP/1.1 XXX ") - 1) + sizeof("<head><title>") - 1 + sizeof("<html></title></head>") - 1 + sizeof("<body><h1>") - 1 + sizeof("</h1></body></html>") - 1;
_itoa( dwContentLength, ach, 10 );
bBodyBuilt = strBody.Append("Content-Length: ", sizeof("Content-Length: ") - 1) && strBody.Append(ach) && strBody.Append("\r\n\r\n", sizeof("\r\n\r\n") - 1) && strBody.Append("<html><head><title>", sizeof("<html><head><title>") - 1) && strBody.Append(strResp.QueryStr() + sizeof("HTTP/1.1 XXX ") - 1, dwRespLength - (sizeof("HTTP/1.1 XXX ") - 1)) && strBody.Append("</title></head>", sizeof("</title></head>") - 1) && strBody.Append("<body><h1>", sizeof("<body><h1>") - 1) && strBody.Append( strResp.QueryStr(), dwRespLength) && strBody.Append( "</h1></body></html>", sizeof("</h1></body></html>") - 1 ); } else { dwContentLength = strError.QueryCCH() + sizeof("</body>") - 1 + sizeof("<html></html>") - 1;
_itoa( dwContentLength, ach, 10 );
bBodyBuilt = strBody.Append("Content-Length: ", sizeof("Content-Length: ") - 1) && strBody.Append(ach) && strBody.Append("\r\n\r\n<html>", sizeof("\r\n\r\n<html>") - 1) && strBody.Append(strError) && strBody.Append("</body></html>", sizeof("</body></html>") - 1); }
}
if (!bBodyBuilt) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Failed to build error message for error %d, aborting connecting\n", HTResponse)); goto DisconnectNow;
}
pRequest->SetKeepConn(FALSE); pRequest->SetAuthenticationRequested(FALSE); fDone = FALSE;
if ( !pRequest->BuildBaseResponseHeader( pRequest->QueryRespBuf(), &fDone, &strResp, HTTPH_NO_CUSTOM)) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Failed to send status (error %d), aborting connecting\n", ::GetLastError()));
goto DisconnectNow; }
DBG_ASSERT(!fDone);
pszResp = pRequest->QueryRespBufPtr(); dwRespLength = strlen(pszResp);
if (HTResponse == HT_SVC_UNAVAILABLE) { CHAR *pszRespEnd;
// Need to append a 'Retry-After' header in this case.
if ((dwRespLength + 1 + g_dwPutTimeoutStrlen) > pRequest->QueryRespBuf()->QuerySize()) { // Resize the buffer.
if (!pRequest->QueryRespBuf()->Resize(dwRespLength + 1 + g_dwPutTimeoutStrlen)) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Unable to resize response buf for status %d, aborting connecting\n", ::GetLastError()));
goto DisconnectNow; }
pszResp = pRequest->QueryRespBufPtr(); }
pszRespEnd = pszResp + dwRespLength;
APPEND_STRING(pszRespEnd, "Retry-After: "); memcpy(pszRespEnd, g_szPutTimeoutString, g_dwPutTimeoutStrlen); pszRespEnd += g_dwPutTimeoutStrlen; APPEND_STRING(pszRespEnd, "\r\n");
dwRespLength = DIFF(pszRespEnd - pszResp); }
if (HTResponse == HT_METHOD_NOT_ALLOWED) {
if ((dwRespLength + 1 + MAX_ALLOW_SIZE) > pRequest->QueryRespBuf()->QuerySize()) { // Resize the buffer.
if (!pRequest->QueryRespBuf()->Resize(dwRespLength + 1 + MAX_ALLOW_SIZE)) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Unable to resize response buf for status %d, aborting connecting\n", ::GetLastError()));
goto DisconnectNow; }
pszResp = pRequest->QueryRespBufPtr(); }
dwRespLength += pRequest->BuildAllowHeader( pRequest->QueryURL(), pszResp + dwRespLength ); }
if ((dwRespLength + 1 + strBody.QueryCB()) > pRequest->QueryRespBuf()->QuerySize() ) { if (!pRequest->QueryRespBuf()->Resize(dwRespLength + 1 + strBody.QueryCB())) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Unable to resize response buf for status %d, aborting connecting\n", ::GetLastError()));
goto DisconnectNow; }
pszResp = pRequest->QueryRespBufPtr(); }
// If this is an error response to a head request, truncate it now.
if (pRequest->QueryVerb() == HTV_HEAD) { CHAR *pszHeaderEnd; DWORD dwBodySize;
pszHeaderEnd = strstr(strBody.QueryStr(), "\r\n\r\n");
if (pszHeaderEnd == NULL) { DBG_ASSERT(FALSE); goto DisconnectNow; }
dwBodySize = DIFF(pszHeaderEnd - strBody.QueryStr()) + sizeof("\r\n\r\n") - 1; strBody.SetLen(dwBodySize); }
memcpy( pszResp + dwRespLength, strBody.QueryStr(), strBody.QueryCCH() + 1 );
if ( !pRequest->SendHeader( pszResp, dwRespLength + strBody.QueryCCH(), IO_FLAG_ASYNC, &fDone )) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Failed to send status (error %d), aborting connecting\n", ::GetLastError()));
//
// It's possible a filter failed the writefile, cause the
// filter code to issue a disconnect by the time we get here,
// so recheck the state
//
if ( QueryState() != CCS_SHUTDOWN ) { goto DisconnectNow; } }
if ( fDone ) { goto DisconnectNow; } } else { DisconnectNow:
IF_DEBUG( CONNECTION ) { DBGPRINTF((DBG_CONTEXT, "Disconnect: Going to Shutdown for %lx, ref count = %d\n", this, QueryRefCount() )); }
if ( pfFinished ) { *pfFinished = TRUE; }
SetState( CCS_SHUTDOWN );
//
// Do a shutdown to avoid a nasty client reset when:
//
// The client sent more entity data then they indicated (very common)
// and we may not have received all of it in our initial receive
// buffer OR
//
// This was a CGI request. The exiting process will cause the
// socket handle to be deleted but NT will force a reset on the
// socket because the process didn't do a close/shutdown (the
// process inherited this socket handle). Atq does the right thing
// when using TransmitFile.
//
#if CC_REF_TRACKING
CC_LOG_REF_COUNT( -_cRef ); #endif
AtqCloseSocket( QueryAtqContext(), fDoShutdown );
//
// This removes the last reference count to *this except for
// any outstanding IO requests
//
Dereference(); } }
/*******************************************************************
NAME: CLIENT_CONN::DisconnectAllUsers
SYNOPSIS: Static method that walks the connection list and disconnects each active connection.
ENTRY: Instance - Pointer to a server instance. If NULL, then all users are disconnected. If !NULL, then only those users associated with the specified instance are disconnected.
HISTORY: Johnl 02-Sep-1994 Created
********************************************************************/
VOID CLIENT_CONN::DisconnectAllUsers( PIIS_SERVER_INSTANCE Instance ) { LIST_ENTRY * pEntry; CLIENT_CONN * pconn;
DBGPRINTF((DBG_CONTEXT, "DisconnectAllUsers entered\n")); LockGlobals();
for ( pEntry = listConnections.Flink; pEntry != &listConnections; pEntry = pEntry->Flink ) { pconn = CONTAINING_RECORD( pEntry, CLIENT_CONN, ListEntry ); DBG_ASSERT( pconn->CheckSignature() );
if( Instance == NULL || Instance == (PIIS_SERVER_INSTANCE)pconn->QueryW3Instance() ) {
#if CC_REF_TRACKING
//
// log to our various ref logs
//
// NOTE negative indicates no change to ref count
//
//
// log to local (per-object) CLIENT_CONN log
//
LogRefCountCCLocal( - pconn->_cRef , pconn , pconn->_phttpReq , NULL , (pconn->_phttpReq ? pconn->_phttpReq->QueryState() : 0) ); #endif
//
// If we've already posted a TransmitFile() for the connection
// before being asked
// to shutdown, then AtqCloseSocket() by itself will not close
// the socket (because by default, we will pass TF_DISCONNECT|
// TS_REUSE_SOCKET and ATQ will assume WinSock will take care of
// it). The end result is that we will be stuck
// waiting for the completion (for 2 minutes). Let's force the
// issue
//
AtqContextSetInfo( pconn->QueryAtqContext(), ATQ_INFO_FORCE_CLOSE, TRUE );
AtqCloseSocket( pconn->QueryAtqContext(), FALSE ); } }
UnlockGlobals(); }
/*******************************************************************
NAME: CLIENT_CONN::Reference
SYNOPSIS: Increments the reference count.
HISTORY: DaveK 10-Sep-1997 Added ref trace logging
********************************************************************/
UINT CLIENT_CONN::Reference( VOID ) { LONG cRefs = InterlockedIncrement( &_cRef );
CC_LOG_REF_COUNT( cRefs );
return cRefs; }
/*******************************************************************
NAME: CLIENT_CONN::Dereference
SYNOPSIS: Increments the reference count.
HISTORY: DaveK 10-Sep-1997 Added ref trace logging
********************************************************************/
UINT CLIENT_CONN::Dereference( VOID ) { //
// Write the trace log BEFORE the decrement operation :(
// If we write it after the decrement, we will run into potential
// race conditions in this object getting freed up accidentally
// by another thread
//
// NOTE we write (_cRef - 1) == ref count AFTER decrement happens
//
LONG cRefsAfter = (_cRef - 1); CC_LOG_REF_COUNT( cRefsAfter );
return InterlockedDecrement( &_cRef ); }
/*******************************************************************
More CLIENT_CONN methods
SYNOPSIS: More methods. . HISTORY: DaveK 10-Sep-1997 Added this comment
********************************************************************/
DWORD CLIENT_CONN::Initialize( VOID ) { DWORD msScavengeTime = ACACHE_REG_DEFAULT_CLEANUP_INTERVAL; HKEY hkey;
INITIALIZE_CRITICAL_SECTION( &_csBuffList ); InitializeListHead( &_BuffListHead );
#if CC_REF_TRACKING
g_pDbgCCRefTraceLog = CreateRefTraceLog( C_CLIENT_CONN_REFTRACES, 0 ); #endif
if ( RegOpenKeyEx( HKEY_LOCAL_MACHINE, ACACHE_REG_PARAMS_REG_KEY, 0, KEY_READ, &hkey ) == NO_ERROR ) { msScavengeTime = ReadRegistryDword( hkey, ACACHE_REG_LOOKASIDE_CLEANUP_INTERVAL, ACACHE_REG_DEFAULT_CLEANUP_INTERVAL ); DBG_REQUIRE( !RegCloseKey( hkey )); }
msScavengeTime *= 1000; // Convert to milliseconds
_FreeListScavengerCookie = ScheduleWorkItem( ClientConnTrimScavenger, NULL, msScavengeTime, TRUE );
_fGlobalInit = TRUE; return NO_ERROR; }
VOID CLIENT_CONN::Terminate( VOID ) { CLIENT_CONN * pConn;
if ( !_fGlobalInit ) { return; }
RemoveWorkItem( _FreeListScavengerCookie );
EnterCriticalSection( &_csBuffList );
while ( !IsListEmpty( &_BuffListHead )) { pConn = CONTAINING_RECORD( _BuffListHead.Flink, CLIENT_CONN, _BuffListEntry );
DBG_ASSERT( pConn->_Signature == CLIENT_CONN_SIGNATURE_FREE );
RemoveEntryList( &pConn->_BuffListEntry );
delete pConn; }
LeaveCriticalSection( &_csBuffList ); DeleteCriticalSection( &_csBuffList );
#if CC_REF_TRACKING
DestroyRefTraceLog( g_pDbgCCRefTraceLog ); g_pDbgCCRefTraceLog = NULL; #endif
}
CLIENT_CONN * CLIENT_CONN::Alloc( IN PCLIENT_CONN_PARAMS ClientParams ) { CLIENT_CONN * pConn;
EnterCriticalSection( &_csBuffList );
if ( !IsListEmpty( &_BuffListHead )) { pConn = CONTAINING_RECORD( _BuffListHead.Flink, CLIENT_CONN, _BuffListEntry );
RemoveEntryList( &pConn->_BuffListEntry );
_cFree--; LeaveCriticalSection( &_csBuffList );
DBG_ASSERT( pConn->_Signature == CLIENT_CONN_SIGNATURE_FREE ); pConn->Initialize( ClientParams ); DBG_ASSERT( pConn->CheckSignature() );
return pConn; }
LeaveCriticalSection( &_csBuffList );
return new CLIENT_CONN( ClientParams ); }
VOID CLIENT_CONN::Free( CLIENT_CONN * pConn ) { DBG_ASSERT( pConn->CheckSignature() );
pConn->Reset();
EnterCriticalSection( &_csBuffList ); InsertHeadList( &_BuffListHead, &pConn->_BuffListEntry );
_cFree++; LeaveCriticalSection( &_csBuffList ); return; }
VOID CLIENT_CONN::TrimFreeList( VOID ) { CLIENT_CONN * pConn; LIST_ENTRY ListEntry;
// Hold the lock only to reset the list to empty. Do the actual
// traversal and deletion out side the lock.
EnterCriticalSection( &_csBuffList );
if ( IsListEmpty( &_BuffListHead ) ) { LeaveCriticalSection( &_csBuffList ); DBG_ASSERT( _cFree == 0 ); return; } else { ListEntry = _BuffListHead; ListEntry.Blink->Flink = &ListEntry; ListEntry.Flink->Blink = &ListEntry; _cFree = 0; InitializeListHead( &_BuffListHead ); }
LeaveCriticalSection( &_csBuffList );
while ( !IsListEmpty( &ListEntry )) { pConn = CONTAINING_RECORD( ListEntry.Flink, CLIENT_CONN, _BuffListEntry );
DBG_ASSERT( pConn->_Signature == CLIENT_CONN_SIGNATURE_FREE );
RemoveEntryList( &pConn->_BuffListEntry );
delete pConn; } }
VOID WINAPI ClientConnTrimScavenger( PVOID pContext ) { CLIENT_CONN::TrimFreeList(); }
/*******************************************************************
NAME: ::W3Completion
SYNOPSIS: Completion routine for W3 Atq requests
HISTORY: Johnl 20-Aug-1994 Created
********************************************************************/
VOID W3Completion( PVOID Context, DWORD BytesWritten, DWORD CompletionStatus, OVERLAPPED * lpo ) { CLIENT_CONN * pConn = (CLIENT_CONN *) Context;
DBG_ASSERT( pConn ); DBG_ASSERT( pConn->CheckSignature() );
#if 0
DBGPRINTF((DBG_CONTEXT, "W3Completion( %08lx ) byteswritten = %d, status = %08lx, lpo = %08lx\n", Context, BytesWritten, CompletionStatus, lpo )); #endif
if ( !((W3_IIS_SERVICE*)g_pInetSvc)->GetReferenceCount() ) { return; } W3_IIS_SERVICE::ReferenceW3Service( g_pInetSvc );
#if CC_REF_TRACKING
//
// ATQ notification trace
//
// Check for magic signature of ATQ notification
// ATQ generates such a notification for all non-oplock completion
//
if ( ((DWORD)(LPVOID)lpo & 0xf0f0f0f0) == 0xf0f0f0f0 ) { pConn->NotifyAtqProcessContext( BytesWritten, CompletionStatus, (DWORD)(LPVOID)lpo ); W3_IIS_SERVICE::DereferenceW3Service( g_pInetSvc ); return; } #endif
ReferenceConn( pConn );
if ( lpo != NULL ) { lpo->Offset = 0; }
TCP_REQUIRE( pConn->DoWork( BytesWritten, CompletionStatus, lpo != NULL ));
DereferenceConn( pConn );
W3_IIS_SERVICE::DereferenceW3Service( g_pInetSvc ); }
#if CC_REF_TRACKING
VOID CLIENT_CONN::NotifyAtqProcessContext( DWORD BytesWritten, DWORD CompletionStatus, DWORD dwSig ) /*++
Routine Description:
Store ATQ notification in ref log
Arguments:
BytesWritten - count of bytes written CompletionStatus - completion status
Return Value:
Nothing
--*/ { //
// Store current ref count as checkpoint
//
CCA_LOG_REF_COUNT( -_cRef, BytesWritten, CompletionStatus, dwSig ); } #endif
/*******************************************************************
NAME: ::CheckForTermination
SYNOPSIS: Looks in the passed buffer for a line followed by a blank line. If not found, the buffer is resized.
ENTRY: pfTerminted - Set to TRUE if this block is terminated pbuff - Pointer to buffer data cbData - Size of pbuff ppbExtraData - Receives a pointer to the first byte of extra data following the header pcbExtraData - Number of bytes in data following the header cbReallocSize - Increase buffer by this number of bytes if the terminate isn't found
RETURNS: TRUE if successful, FALSE otherwise
HISTORY: Johnl 28-Sep-1994 Created
********************************************************************/
BOOL CheckForTermination( BOOL * pfTerminated, BUFFER * pbuff, UINT cbData, BYTE * * ppbExtraData, DWORD * pcbExtraData, UINT cbReallocSize ) { DWORD cbNewSize;
cbNewSize = cbData + 1 + cbReallocSize;
if ( !pbuff->Resize( cbNewSize ) ) { return FALSE; }
//
// Terminate the string but make sure it will fit in the
// buffer
//
CHAR * pchReq = (CHAR *) pbuff->QueryPtr(); *(pchReq + cbData) = '\0';
//
// Scan for double end of line marker
//
//
// if do not care for ptr info, can use fast method
//
if ( ppbExtraData == NULL ) { if ( FastScanForTerminator( pchReq, cbData ) || ScanForTerminator( pchReq ) ) { *pfTerminated = TRUE; return TRUE; } goto not_term; }
*ppbExtraData = ScanForTerminator( pchReq );
if ( *ppbExtraData ) { *pcbExtraData = cbData - DIFF(*ppbExtraData - (BYTE *) pchReq); *pfTerminated = TRUE; return TRUE; }
not_term:
if ( cbNewSize > g_cbMaxClientRequestBuffer ) { SetLastError( ERROR_INSUFFICIENT_BUFFER ); return FALSE; }
*pfTerminated = FALSE;
return TRUE; }
BOOL CLIENT_CONN::RequestAbortiveClose( ) /*++
Routine Description:
Request for abortive close on disconnect
Arguments:
None
Returns:
TRUE if successful, else FALSE
--*/ { _fAbortiveClose = TRUE;
AtqContextSetInfo( QueryAtqContext(), ATQ_INFO_ABORTIVE_CLOSE, TRUE );
return TRUE; }
BOOL CLIENT_CONN::CloseConnection( ) /*++
Routine Description:
Cancels any pending async IO by closing the socket
Arguments:
None
Returns:
TRUE if successful, else FALSE
--*/ { return AtqCloseSocket( QueryAtqContext(), FALSE ); }
#if 0
BOOL CLIENT_CONN::CheckIpAccess( LPBOOL pfGranted, LPBOOL pfNeedDns, LPBOOL pfComplete ) /*++
Routine Description:
Check IP access granted
Arguments:
pfGranted - updated with grant status pfNeedDns - updated with TRUE if need DNS name for DNS check pfComplete - updated with TRUE is check complete
Return Value:
TRUE if no error, otherwise FALSE
--*/ { return _acCheck.CheckIpAccess( pfGranted, pfNeedDns, pfComplete ); }
BOOL CLIENT_CONN::CheckDnsAccess( LPBOOL pfGranted ) /*++
Routine Description:
Check DNS access granted
Arguments:
pfGranted - updated with grant status
Return Value:
TRUE if no error, otherwise FALSE
--*/ { return _acCheck.CheckDnsAccess( pfGranted ); } #endif
VOID DumpW3InfoToHTML( OUT CHAR * pch, IN OUT LPDWORD pcb ) { DWORD cb = 0;
DBG_ASSERT( *pcb > 1024 );
cb += wsprintf( pch + cb, "<TABLE BORDER>" ); cb += wsprintf( pch + cb, "<TR><TD>Current Connections: </TD><TD>%d</TD></TR>", g_pW3Stats->QueryCurrentConnections() ); cb += wsprintf( pch + cb, "<TR><TD>Connections Attempts: </TD><TD>%d</TD></TR>", g_pW3Stats->QueryConnectionAttempts() ); cb += wsprintf( pch + cb, "<TR><TD>Current CGIs: </TD><TD>%d</TD></TR>", g_pW3Stats->QueryCurrentCGIRequests() ); cb += wsprintf( pch + cb, "<TR><TD>Total CGIs: </TD><TD>%d</TD></TR>", g_pW3Stats->QueryTotalCGIRequests() ); cb += wsprintf( pch + cb, "<TR><TD>Current ISAPI Requests: </TD><TD>%d</TD></TR>", g_pW3Stats->QueryCurrentBGIRequests() ); cb += wsprintf( pch + cb, "<TR><TD>Total ISAPI Requests: </TD><TD>%d</TD></TR>", g_pW3Stats->QueryTotalBGIRequests() ); cb += wsprintf( pch + cb, "<TR><TD>Current Connections: </TD><TD>%d</TD></TR>", g_pW3Stats->QueryCurrentConnections() ); cb += wsprintf( pch + cb, "<TR><TD>Free CLIENT_CONN list: </TD><TD>%d</TD></TR>", CLIENT_CONN::QueryFreeListSize() ); cb += wsprintf( pch + cb, "</TABLE><p>" );
DBG_ASSERT( *pcb > cb );
*pcb = cb; }
|