Windows NT 4.0 source code leak
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.
 
 
 
 
 
 

1152 lines
29 KiB

/**********************************************************************/
/** 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"
#pragma warning( disable:4355 ) // 'this' used in base member initializer list
//
// Private constants.
//
//
// Private globals.
//
CRITICAL_SECTION CLIENT_CONN::_csBuffList;
LIST_ENTRY CLIENT_CONN::_BuffListHead;
BOOL CLIENT_CONN::_fGlobalInit = FALSE;
//
// Private prototypes.
//
BYTE *
ScanForTerminator(
TCHAR * pch
);
//
// Public functions.
//
//
// 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( SOCKET sClient,
SOCKADDR_IN * psockaddrLocal,
SOCKADDR_IN * psockaddrRemote,
PATQ_CONTEXT patqContext,
PVOID pvInitialBuff,
DWORD cbInitialBuff
) :
_phttpReq( NULL )
{
Initialize( sClient,
psockaddrLocal,
psockaddrRemote,
patqContext,
pvInitialBuff,
cbInitialBuff );
}
VOID
CLIENT_CONN::Initialize(
SOCKET sClient,
SOCKADDR_IN * psockaddrLocal,
SOCKADDR_IN * psockaddrRemote,
PATQ_CONTEXT patqContext,
PVOID pvInitialBuff,
DWORD cbInitialBuff
)
/*++
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;
struct sockaddr_in sockaddr;
int cbAddr = sizeof( sockaddr );
_Signature = CLIENT_CONN_SIGNATURE;
_sClient = sClient;
_ccState = CCS_STARTUP;
_cRef = 1;
_AtqContext = patqContext;
_fIsValid = FALSE;
_fReuseContext = TRUE;
_pvInitial = pvInitialBuff;
_cbInitial = cbInitialBuff;
IF_DEBUG( CONNECTION )
{
TCP_PRINT((DBG_CONTEXT,
"Initializing connection object %lx, new user count %d\n",
this,
cConnectedUsers + 1 ));
}
//
// Put the item on the connection list even if we fail below. When the
// is freed, it will be removed from the connection list
//
LockGlobals();
cConnectedUsers++;
InsertHeadList( &listConnections, &ListEntry );
UnlockGlobals();
INCREMENT_COUNTER( CurrentConnections );
if ( W3Stats.CurrentConnections > W3Stats.MaxConnections )
{
LockStatistics();
if ( W3Stats.CurrentConnections > W3Stats.MaxConnections )
W3Stats.MaxConnections = W3Stats.CurrentConnections;
UnlockStatistics();
}
InetNtoa( psockaddrRemote->sin_addr, _achRemoteAddr );
if ( psockaddrLocal )
{
InetNtoa( psockaddrLocal->sin_addr, _achLocalAddr );
_sPort = ntohs( psockaddrLocal->sin_port );
}
else
{
if ( getsockname( sClient,
(struct sockaddr *) &sockaddr,
&cbAddr ))
{
TCP_PRINT((DBG_CONTEXT,
"[CLIENT_CONN] inet_ntoa failed\n"));
return;
}
InetNtoa( sockaddr.sin_addr, _achLocalAddr );
_sPort = ntohs( sockaddr.sin_port );
}
TCP_ASSERT( (_tcslen( _achLocalAddr ) + 1) <= sizeof(_achLocalAddr));
if ( _phttpReq )
{
_phttpReq->InitializeSession( this,
pvInitialBuff,
cbInitialBuff );
}
else
{
_phttpReq = HTTP_REQUEST::Alloc(this,
pvInitialBuff,
cbInitialBuff );
if ( !_phttpReq )
{
SetLastError( ERROR_NOT_ENOUGH_MEMORY );
return;
}
}
_fIsValid = TRUE;
}
CLIENT_CONN::~CLIENT_CONN()
{
delete _phttpReq;
_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:
--*/
{
IF_DEBUG( CONNECTION )
{
TCP_PRINT((DBG_CONTEXT,
"Resetting connection object %lx, AtqCont = %lx new user count %d\n",
this,
_AtqContext,
cConnectedUsers - 1 ));
}
if ( _phttpReq )
{
_phttpReq->SessionTerminated();
}
if ( QueryAtqContext() )
{
AtqFreeContext( QueryAtqContext(), _fReuseContext );
_AtqContext = NULL;
}
//
// Remove ourselves from the connection list and knock down our
// connected user count
//
LockGlobals();
RemoveEntryList( &ListEntry );
cConnectedUsers--;
TCP_ASSERT( ((LONG) cConnectedUsers) >= 0 );
UnlockGlobals();
DECREMENT_COUNTER( CurrentConnections );
_Signature = CLIENT_CONN_SIGNATURE_FREE;
}
/*******************************************************************
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;
IF_DEBUG( CONNECTION )
{
TCP_PRINT(( DBG_CONTEXT,
"DoWork: Object %lx Last IO error %lu Bytes = %d State = %d\n"
"\tIsIO = %s Ref = %d Sock = %x\n",
this,
CompletionStatus,
BytesWritten,
QueryState(),
fIOCompletion ? "TRUE" : "FALSE",
QueryRefCount(),
(QueryAtqContext() ? (int) QueryAtqContext()->hAsyncIO : 0xffff) ));
}
//
// If this was a completion generated by an IO request, decrement the
// ref count
//
if ( fIOCompletion )
{
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.
//
_phttpReq->SetLastCompletionStatus( BytesWritten,
CompletionStatus );
if ( CompletionStatus && !IsCleaningUp() )
{
//
// If an error occurred on an async operation, set the win32 log
// status code to reflect the error but don't reset the HTTP status
// code.
//
if ( CompletionStatus != ERROR_OPERATION_ABORTED )
{
_phttpReq->SetLogStatus( _phttpReq->QueryLogHttpResponse(),
CompletionStatus );
}
TCP_PRINT(( DBG_CONTEXT,
"DoWork: Aborting client connection, error %d\n",
CompletionStatus ));
if ( HTR_GATEWAY_ASYNC_IO == _phttpReq->QueryState()) {
// 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();
}
Disconnect( NULL, 0, 0, FALSE );
goto Exit;
}
switch ( QueryState() )
{
case CCS_STARTUP:
//
// Do this at the beginning of every request
//
fRet = OnSessionStartup( _pvInitial,
_cbInitial );
if ( !fRet || !_pvInitial )
{
break;
}
//
// Fall through
//
case CCS_PROCESSING_CLIENT_REQ:
fRet = _phttpReq->DoWork( &fFinished );
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()) )
{
//
// Either we completed the disconnect so
// close the socket or an error
// occurred setting up for the next request w/ KeepConn set
//
Disconnect();
}
}
}
break;
case CCS_DISCONNECTING:
Disconnecting:
//
// Write the log record for this request
//
_phttpReq->WriteLogRecord();
Disconnect();
break;
case CCS_SHUTDOWN:
//
// Nothing to do but wait for the async IOs to complete
//
break;
default:
fRet = FALSE;
TCP_ASSERT( FALSE );
}
//
// If an error occurred, disconnect without sending a response
//
if ( !fRet )
{
_phttpReq->SetLogStatus( HT_SERVER_ERROR, GetLastError() );
_phttpReq->Disconnect( HT_SERVER_ERROR, GetLastError() );
}
Exit:
IF_DEBUG( CONNECTION )
{
TCP_PRINT((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
//
TCP_ASSERT( QueryRefCount() > 0 );
return TRUE;
}
/*******************************************************************
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(
PVOID pvInitial,
DWORD cbInitial
)
{
APIERR err = NO_ERROR;
//
// Associate our client socket with Atq if it hasn't already
//
if ( !QueryAtqContext() )
{
TCP_ASSERT( pvInitial == NULL );
if ( !AtqAddAsyncHandle( &_AtqContext,
this,
W3Completion,
g_pTsvcInfo->QueryConnectionTimeout(),
(HANDLE) QuerySocket()))
{
TCP_PRINT(( DBG_CONTEXT,
"OnSessionStartup: failed to add Atq handle, error %lu\n",
GetLastError() ));
return FALSE;
}
}
SetState( CCS_PROCESSING_CLIENT_REQ );
return _phttpReq->StartNewRequest( pvInitial,
cbInitial );
}
/*******************************************************************
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 )
{
CHAR * pszResp;
//
// If Disconnect has already been called, then this is a no-op
//
if ( QueryState() == CCS_SHUTDOWN )
{
return;
}
if ( pRequest && HTResponse )
{
STR strBody;
if ( HTResponse == HT_NOT_FOUND )
{
INCREMENT_COUNTER( TotalNotFoundErrors );
}
//
// Means we have to wait for the status response before closing
// the socket
//
SetState( CCS_DISCONNECTING );
IF_DEBUG( CONNECTION )
{
TCP_PRINT((DBG_CONTEXT,
"Disconnect: Going to Disconnecting for %lx, ref = %d, response = %d\n",
this,
QueryRefCount(),
HTResponse ));
}
//
// Send the requested status response and after that completes close
// the socket
//
if ( !HTTP_REQ_BASE::BuildStatusLine( pRequest->QueryRespBuf(),
HTResponse,
ErrorResponse, pRequest->QueryURL() ) ||
!strBody.Copy( "Content-Type: text/html\r\n\r\n"
"<body><h1>" ) ||
!strBody.Append( pRequest->QueryRespBufPtr() ) ||
!strBody.Append( "</h1></body>" ))
{
TCP_PRINT((DBG_CONTEXT,
"Disconnect: Failed to send status (error %d), aborting connecting\n",
::GetLastError()));
goto DisconnectNow;
}
pszResp = pRequest->QueryRespBufPtr();
strcat( pszResp,
strBody.QueryStr() );
TCP_ASSERT( strlen( pszResp ) + 1 <= pRequest->QueryRespBuf()->QuerySize() );
if ( !pRequest->WriteFile( pszResp,
strlen( pszResp ),
NULL,
IO_FLAG_ASYNC ))
{
TCP_PRINT((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;
}
}
}
else
{
DisconnectNow:
IF_DEBUG( CONNECTION )
{
TCP_PRINT((DBG_CONTEXT,
"Disconnect: Going to Shutdown for %lx, ref count = %d\n",
this,
QueryRefCount() ));
}
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.
//
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.
HISTORY:
Johnl 02-Sep-1994 Created
********************************************************************/
VOID CLIENT_CONN::DisconnectAllUsers( VOID )
{
LIST_ENTRY * pEntry;
CLIENT_CONN * pconn;
TCP_PRINT((DBG_CONTEXT,
"DisconnectAllUsers entered\n"));
LockGlobals();
for ( pEntry = listConnections.Flink;
pEntry != &listConnections;
pEntry = pEntry->Flink )
{
pconn = CONTAINING_RECORD( pEntry, CLIENT_CONN, ListEntry );
TCP_ASSERT( pconn->CheckSignature() );
AtqCloseSocket( pconn->QueryAtqContext(),
FALSE );
}
UnlockGlobals();
}
/*******************************************************************
NAME: CLIENT_CONN::ReadFile
SYNOPSIS: Simple wrapper around AtqReadFile
HISTORY:
Johnl 24-Aug-1994 Created
********************************************************************/
BOOL CLIENT_CONN::ReadFile( LPVOID lpBuffer,
DWORD BytesToRead )
{
Reference();
if ( !AtqReadFile( QueryAtqContext(),
lpBuffer,
BytesToRead,
NULL ))
{
Dereference();
return FALSE;
}
return TRUE;
}
/*******************************************************************
NAME: CLIENT_CONN::WriteFile
SYNOPSIS: Simple wrapper around AtqWriteFile
HISTORY:
Johnl 24-Aug-1994 Created
********************************************************************/
BOOL CLIENT_CONN::WriteFile( LPVOID lpBuffer,
DWORD BytesToWrite )
{
Reference();
if ( !AtqWriteFile( QueryAtqContext(),
lpBuffer,
BytesToWrite,
NULL ))
{
Dereference();
return FALSE;
}
return TRUE;
}
/*******************************************************************
NAME: CLIENT_CONN::TransmitFile
SYNOPSIS: Simple wrapper around AtqTransmitFile
HISTORY:
Johnl 24-Aug-1994 Created
********************************************************************/
BOOL CLIENT_CONN::TransmitFile( HANDLE hFile,
DWORD BytesToWrite,
DWORD dwFlags,
PVOID pHead,
DWORD HeadLength,
PVOID pTail,
DWORD TailLength )
{
TRANSMIT_FILE_BUFFERS tfb;
LARGE_INTEGER li;
dwFlags &= (TF_DISCONNECT | TF_REUSE_SOCKET);
li.QuadPart = BytesToWrite;
tfb.Head = pHead;
tfb.HeadLength = HeadLength;
tfb.Tail = pTail;
tfb.TailLength = TailLength;
Reference();
if ( !AtqTransmitFile( QueryAtqContext(),
hFile,
li,
&tfb,
dwFlags ))
{
Dereference();
return FALSE;
}
return TRUE;
}
/*******************************************************************
NAME: CLIENT_CONN::TransmitFileEx
SYNOPSIS: Simple wrapper around AtqTransmitFileEx2
HISTORY:
Johnl 24-Aug-1994 Created
********************************************************************/
BOOL CLIENT_CONN::TransmitFileEx( HANDLE hFile,
DWORD Offset,
DWORD BytesToWrite,
DWORD dwFlags,
PVOID pHead,
DWORD HeadLength,
PVOID pTail,
DWORD TailLength )
{
TRANSMIT_FILE_BUFFERS tfb;
LARGE_INTEGER li;
dwFlags &= (TF_DISCONNECT | TF_REUSE_SOCKET);
li.QuadPart = BytesToWrite;
tfb.Head = pHead;
tfb.HeadLength = HeadLength;
tfb.Tail = pTail;
tfb.TailLength = TailLength;
Reference();
QueryAtqContext()->Overlapped.Offset = Offset;
if ( !AtqTransmitFileEx( QueryAtqContext(),
hFile,
li,
&tfb,
dwFlags,0,0 ))
{
Dereference();
return FALSE;
}
return TRUE;
}
DWORD
CLIENT_CONN::Initialize(
VOID
)
{
InitializeCriticalSection( &_csBuffList );
InitializeListHead( &_BuffListHead );
_fGlobalInit = TRUE;
return NO_ERROR;
}
VOID
CLIENT_CONN::Terminate(
VOID
)
{
CLIENT_CONN * pConn;
if ( !_fGlobalInit )
return;
EnterCriticalSection( &_csBuffList );
while ( !IsListEmpty( &_BuffListHead ))
{
pConn = CONTAINING_RECORD( _BuffListHead.Flink,
CLIENT_CONN,
_BuffListEntry );
TCP_ASSERT( pConn->_Signature == CLIENT_CONN_SIGNATURE_FREE );
RemoveEntryList( &pConn->_BuffListEntry );
delete pConn;
}
LeaveCriticalSection( &_csBuffList );
DeleteCriticalSection( &_csBuffList );
}
CLIENT_CONN *
CLIENT_CONN::Alloc(
SOCKET sClient,
SOCKADDR_IN * psockaddrLocal,
SOCKADDR_IN * psockaddrRemote,
PATQ_CONTEXT patqContext,
PVOID pvInitialBuff,
DWORD cbInitialBuff
)
{
CLIENT_CONN * pConn;
EnterCriticalSection( &_csBuffList );
if ( !IsListEmpty( &_BuffListHead ))
{
pConn = CONTAINING_RECORD( _BuffListHead.Flink,
CLIENT_CONN,
_BuffListEntry );
RemoveEntryList( &pConn->_BuffListEntry );
LeaveCriticalSection( &_csBuffList );
TCP_ASSERT( pConn->_Signature == CLIENT_CONN_SIGNATURE_FREE );
pConn->Initialize( sClient,
psockaddrLocal,
psockaddrRemote,
patqContext,
pvInitialBuff,
cbInitialBuff );
TCP_ASSERT( pConn->CheckSignature() );
return pConn;
}
LeaveCriticalSection( &_csBuffList );
return new CLIENT_CONN( sClient,
psockaddrLocal,
psockaddrRemote,
patqContext,
pvInitialBuff,
cbInitialBuff );
}
VOID
CLIENT_CONN::Free(
CLIENT_CONN * pConn
)
{
TCP_ASSERT( pConn->CheckSignature() );
pConn->Reset();
EnterCriticalSection( &_csBuffList );
InsertHeadList( &_BuffListHead,
&pConn->_BuffListEntry );
LeaveCriticalSection( &_csBuffList );
}
BOOL
CLIENT_CONN::PostCompletionStatus(
DWORD BytesTransferred
)
/*++
Routine Description:
Posts a completion status to this connection's ATQ context
Arguments:
BytesTransferred - Count of bytes sent or received from buffer
Return Value:
TRUE on success, FALSE on failure (call GetLastError)
--*/
{
Reference();
if ( !AtqPostCompletionStatus( QueryAtqContext(),
BytesTransferred ))
{
Dereference();
return FALSE;
}
return TRUE;
}
/*******************************************************************
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;
TCP_ASSERT( pConn );
TCP_ASSERT( pConn->CheckSignature() );
ReferenceConn( pConn );
if ( lpo != NULL )
lpo->Offset = 0;
TCP_REQUIRE( pConn->DoWork( BytesWritten,
CompletionStatus,
lpo != NULL ));
DereferenceConn( pConn );
}
/*******************************************************************
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 )
{
//
// Terminate the string but make sure it will fit in the
// buffer
//
if ( !pbuff->Resize(cbData + 1, cbReallocSize ) )
{
return FALSE;
}
CHAR * pchReq = (CHAR *) pbuff->QueryPtr();
*(pchReq + cbData) = '\0';
//
// Scan for double end of line marker
//
*ppbExtraData = ScanForTerminator( pchReq );
if ( *ppbExtraData )
{
*pcbExtraData = cbData - (*ppbExtraData - (BYTE *) pchReq);
*pfTerminated = TRUE;
return TRUE;
}
*pfTerminated = FALSE;
//
// We didn't find the end so increase our buffer size
// in anticipation of more data
//
return pbuff->Resize( cbData + cbReallocSize );
}
BYTE *
ScanForTerminator(
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
//
if ( *(pch = SkipWhite( pch + 1 )) == W3_EOL )
{
return (BYTE *) pch + 1;
}
else if ( *pch )
{
pch++;
}
}
return NULL;
}