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.
 
 
 
 
 
 

2943 lines
79 KiB

/**********************************************************************/
/** Microsoft Windows NT **/
/** Copyright(c) Microsoft Corp., 1994 **/
/**********************************************************************/
/*
cgi.cxx
This module contains the gateway interface code for an HTTP request
FILE HISTORY:
Johnl 22-Sep-1994 Created
MuraliK 22-Jan-1996 Use UNC Impersonation/Revert
*/
#include "w3p.hxx"
#include "issched.hxx"
//
// This is the exit code given to processes that we terminate
//
#define CGI_PREMATURE_DEATH_CODE 0xf1256323
//
// Beyond around this number, CreateProcessXXX return errors
//
#define CGI_COMMAND_LINE_MAXCB 30000
#define ISUNC(a) ((a)[0]=='\\' && (a)[1]=='\\')
//
// The default minimum number of CGI threads we'll allow to
// run at once. This is for compatibility with IIS 3.0 and
// is based on the default number of ATQ threads in 3.0 (4.0
// defaults to four). Note this is only effective if
// UsePoolThreadForCGI is TRUE (default).
//
#define IIS3_MIN_CGI 10
//
// Prototypes
//
BOOL
IsCmdExe(
const CHAR * pchPath
);
//
// Private globals.
//
BOOL fCGIInitialized = FALSE;
LONG g_cMinCGIs = 0;
//
// Controls whether special command characters are allowed in cmd.exe
// requests
//
BOOL fAllowSpecialCharsInShell = FALSE;
typedef struct CgiEnvTableEntry_ {
TCHAR* m_pszName;
BOOL m_fIsProcessEnv;
UINT m_cchNameLen;
UINT m_cchToCopy; // will be non zero for var to copy from
// process environment. In this case m_pszName
// points to the environment entry to copy
// ( name + '=' + value + '\0' )
// otherwise this entry is to be accessed
// using GetInfo()
} CgiEnvTableEntry;
//
// Environment variable block used for CGI
//
// best if in alphabetical order ( the env list is easier to read )
// but not mandatory.
// Note that the "" ( accessed as HTTP_ALL ) will be expanded to a list
// of unsorted entries, but this list as a whole will be considered to be
// named "HTTP_ALL" for sorting order.
//
CgiEnvTableEntry CGIEnvTable[] =
{
{TEXT("AUTH_TYPE"),FALSE},
{TEXT("AUTH_PASSWORD"),FALSE},
{TEXT("AUTH_USER"),FALSE},
{TEXT("ComSpec"),TRUE},
{TEXT("CERT_COOKIE"), FALSE},
{TEXT("CERT_FLAGS"), FALSE},
{TEXT("CERT_ISSUER"), FALSE},
{TEXT("CERT_SERIALNUMBER"), FALSE},
{TEXT("CERT_SUBJECT"), FALSE},
{TEXT("CONTENT_LENGTH"),FALSE},
{TEXT("CONTENT_TYPE"),FALSE},
{TEXT("GATEWAY_INTERFACE"),FALSE},
{TEXT(""),FALSE}, // Means insert all HTTP_ headers here
{TEXT("HTTPS"),FALSE},
{TEXT("HTTPS_KEYSIZE"),FALSE},
{TEXT("HTTPS_SECRETKEYSIZE"),FALSE},
{TEXT("HTTPS_SERVER_ISSUER"),FALSE},
{TEXT("HTTPS_SERVER_SUBJECT"),FALSE},
{TEXT("INSTANCE_ID"),FALSE},
{TEXT("LOCAL_ADDR"),FALSE},
{TEXT("LOGON_USER"),FALSE},
{TEXT("PATH"),TRUE},
{TEXT("PATH_INFO"),FALSE},
{TEXT("PATH_TRANSLATED"),FALSE},
{TEXT("QUERY_STRING"),FALSE},
{TEXT("REMOTE_ADDR"),FALSE},
{TEXT("REMOTE_HOST"),FALSE},
{TEXT("REMOTE_USER"),FALSE},
{TEXT("REQUEST_METHOD"),FALSE},
{TEXT("SCRIPT_NAME"),FALSE},
{TEXT("SERVER_NAME"),FALSE},
{TEXT("SERVER_PORT"),FALSE},
{TEXT("SERVER_PORT_SECURE"),FALSE},
{TEXT("SERVER_PROTOCOL"),FALSE},
{TEXT("SERVER_SOFTWARE"),FALSE},
{TEXT("SystemRoot"),TRUE},
{TEXT("UNMAPPED_REMOTE_USER"),FALSE},
{TEXT("windir"),TRUE},
{NULL,FALSE}
};
BOOL fForwardServerEnvironmentBlock = TRUE;
//
// Store environment block for IIS process
//
LPSTR g_pszIisEnv = NULL;
CgiEnvTableEntry *g_pEnvEntries = NULL;
VOID
WINAPI
CGITerminateProcess(
PVOID pContext
);
extern "C" int __cdecl
QsortEnvCmp(
const void *pA,
const void *pB )
/*++
Routine Description:
Compare CgiEnvTableEntry using their name entry
Arguments:
pA - pointer to 1st entry
pB - pointer to 2nd entry
Returns:
-1 if 1st entry comes first in sort order,
0 if identical
1 if 2nd entry comes first
--*/
{
LPSTR p1 = ((CgiEnvTableEntry*)pA)->m_pszName;
LPSTR p2 = ((CgiEnvTableEntry*)pB)->m_pszName;
if ( ! p1[0] )
{
p1 = "HTTP_ALL";
}
if ( ! p2[0] )
{
p2 = "HTTP_ALL";
}
return _stricmp( p1, p2 );
}
/*******************************************************************
NAME: CGI_INFO
SYNOPSIS: Simple storage class passed to thread
HISTORY:
Johnl 22-Sep-1994 Created
********************************************************************/
class CGI_INFO
{
public:
CGI_INFO( HTTP_REQUEST * pRequest )
: _pRequest ( pRequest ),
_cbData ( 0 ),
_hStdOut ( NULL ),
_hStdIn ( NULL ),
_hProcess ( NULL ),
_dwSchedCookie ( 0 ),
_fServerPoolThread( FALSE ),
_pExec ( NULL )
{
}
~CGI_INFO( VOID )
{
if ( _hStdOut )
{
if ( !::CloseHandle( _hStdOut ))
{
DBGPRINTF((DBG_CONTEXT,
"[~CGI_INFO] CloseHandle failed on StdOut, %d\n",
GetLastError()));
}
}
if ( _hStdIn )
{
if ( !::CloseHandle( _hStdIn ))
{
DBGPRINTF((DBG_CONTEXT,
"[~CGI_INFO] CloseHandle failed on StdIn, %d\n",
GetLastError()));
}
}
if ( _hProcess )
{
if ( !::CloseHandle( _hProcess ))
{
DBGPRINTF((DBG_CONTEXT,
"[~CGI_INFO] CloseHandle failed on Process, %d\n",
GetLastError()));
}
}
DWORD dwCookie = (DWORD) InterlockedExchange( (LPLONG) &_dwSchedCookie,
0 );
if ( dwCookie != 0 )
{
RemoveWorkItem( dwCookie );
}
}
HTTP_REQUEST * _pRequest;
DWORD _dwSchedCookie; // Scheduled callback cookie
BOOL _fServerPoolThread; // Are we running in a server pool
// thread?
//
// Child process
//
HANDLE _hProcess;
//
// Parent's input and output handles and child's process handle
//
HANDLE _hStdOut;
HANDLE _hStdIn;
//
// Handles input from CGI (headers and additional data)
//
BUFFER _Buff;
UINT _cbData;
LIST_ENTRY _CgiListEntry;
static LIST_ENTRY _CgiListHead;
static CRITICAL_SECTION _csCgiList;
//
// Execution Descriptor block
//
EXEC_DESCRIPTOR * _pExec;
};
//
// Globals
//
CRITICAL_SECTION CGI_INFO::_csCgiList;
LIST_ENTRY CGI_INFO::_CgiListHead;
//
// Private prototypes.
//
BOOL ProcessCGIInput( CGI_INFO * pCGIInfo );
BOOL SetupChildEnv( EXEC_DESCRIPTOR * pExec,
BUFFER * pBuff );
BOOL SetupChildPipes( STARTUPINFO * pstartupinfo,
HANDLE * phParentIn,
HANDLE * phParentOut );
BOOL SetupCmdLine( STR * pstrCmdLine,
const STR & strParams );
DWORD CGIThread( PVOID Param );
BOOL ProcessCGIInput( CGI_INFO * pCGIInfo,
BYTE * buff,
DWORD cbRead,
BOOL * pfReadHeaders,
BOOL * pfDone,
BOOL * pfSkipDisconnect,
DWORD * pdwHttpStatus );
/*******************************************************************/
APIERR
InitializeCGI(
VOID
)
/*++
Routine Description:
Initialize CGI
Arguments:
None
Return Value:
TRUE if successful, FALSE on error
--*/
{
LPVOID pvEnv;
UINT cchStr;
UINT cchIisEnv;
UINT cEnv;
INT chScanEndOfName;
CgiEnvTableEntry * pCgiEnv;
HKEY hkeyParam;
INITIALIZE_CRITICAL_SECTION( &CGI_INFO::_csCgiList );
InitializeListHead( &CGI_INFO::_CgiListHead );
if ( RegOpenKeyEx( HKEY_LOCAL_MACHINE,
W3_PARAMETERS_KEY,
0,
KEY_READ,
&hkeyParam ) == NO_ERROR )
{
fAllowSpecialCharsInShell = !!ReadRegistryDword( hkeyParam,
"AllowSpecialCharsInShell",
FALSE );
fForwardServerEnvironmentBlock = !!ReadRegistryDword(
hkeyParam,
"ForwardServerEnvironmentBlock",
TRUE );
RegCloseKey( hkeyParam );
}
if ( fForwardServerEnvironmentBlock
&& (pvEnv = GetEnvironmentStrings()) )
{
//
// Compute length of environment block and # of variables
// ( excluding block delimiter )
//
cchIisEnv = 0;
cEnv = 0;
while ( cchStr = strlen( ((PSTR)pvEnv) + cchIisEnv ) )
{
cchIisEnv += cchStr + 1;
++cEnv;
}
++cchIisEnv;
//
// store it
//
if ( (g_pszIisEnv = (LPSTR)LocalAlloc(
LMEM_FIXED, cchIisEnv * sizeof(TCHAR))) == NULL )
{
return ERROR_NOT_ENOUGH_MEMORY;
}
CopyMemory(
g_pszIisEnv,
pvEnv,
cchIisEnv * sizeof(TCHAR) );
FreeEnvironmentStrings( (LPTSTR)pvEnv );
pvEnv = (PVOID)g_pszIisEnv;
if ( g_pEnvEntries = new CgiEnvTableEntry [
cEnv + sizeof(CGIEnvTable)/sizeof(CgiEnvTableEntry) ] )
{
cchIisEnv = 0;
cEnv = 0;
//
// add process environment to table
//
while ( cchStr = strlen( ((PSTR)pvEnv) + cchIisEnv ) )
{
g_pEnvEntries[ cEnv ].m_pszName = ((PSTR)pvEnv) + cchIisEnv;
g_pEnvEntries[ cEnv ].m_fIsProcessEnv = TRUE;
// compute length of name : up to '=' char
for ( g_pEnvEntries[ cEnv ].m_cchNameLen = 0 ;
( chScanEndOfName = g_pEnvEntries[ cEnv ].m_pszName
[ g_pEnvEntries[ cEnv ].m_cchNameLen ] )
&& chScanEndOfName != '=' ; )
{
++g_pEnvEntries[ cEnv ].m_cchNameLen;
}
g_pEnvEntries[ cEnv ].m_cchToCopy = cchStr + 1;
cchIisEnv += cchStr + 1;
++cEnv;
}
//
// add CGI environment variables to table
//
for ( pCgiEnv = CGIEnvTable ; pCgiEnv->m_pszName ; ++pCgiEnv )
{
if ( !pCgiEnv->m_fIsProcessEnv )
{
memcpy( g_pEnvEntries + cEnv, pCgiEnv,
sizeof(CgiEnvTableEntry) );
g_pEnvEntries[ cEnv ].m_cchNameLen
= strlen( pCgiEnv->m_pszName );
g_pEnvEntries[ cEnv ].m_cchToCopy = 0;
++cEnv;
}
}
//
// add delimiter entry
//
g_pEnvEntries[ cEnv ].m_pszName = NULL;
qsort( g_pEnvEntries,
cEnv,
sizeof(CgiEnvTableEntry),
QsortEnvCmp );
}
else
{
return ERROR_NOT_ENOUGH_MEMORY;
}
}
else
{
g_pEnvEntries = CGIEnvTable;
}
fCGIInitialized = TRUE;
return NO_ERROR;
} // InitializeCGI
VOID
TerminateCGI(
VOID
)
/*++
Routine Description:
Terminate CGI
Arguments:
None
Return Value:
Nothing
--*/
{
if ( fCGIInitialized )
{
if (g_pszIisEnv != NULL )
{
LocalFree( g_pszIisEnv );
g_pszIisEnv = NULL;
}
if ( g_pEnvEntries && (g_pEnvEntries != CGIEnvTable) )
{
delete [] g_pEnvEntries;
}
DeleteCriticalSection( &CGI_INFO::_csCgiList );
}
} // TerminateCGI
VOID
KillCGIProcess(
VOID
)
/*++
Routine Description:
Kill all CGI process
Arguments:
None
Return Value:
Nothing
--*/
{
CGI_INFO* pCgi;
if ( fCGIInitialized )
{
//
// Kill all outstanding process
//
BOOL bListEmpty = TRUE;
EnterCriticalSection( &CGI_INFO::_csCgiList );
LIST_ENTRY* pEntry;
for ( pEntry = CGI_INFO::_CgiListHead.Flink;
pEntry != &CGI_INFO::_CgiListHead ;
pEntry = pEntry->Flink )
{
pCgi = CONTAINING_RECORD( pEntry,
CGI_INFO,
CGI_INFO::_CgiListEntry );
bListEmpty = FALSE;
if ( pCgi->_hProcess )
{
CGITerminateProcess( pCgi->_hProcess );
}
}
LeaveCriticalSection( &CGI_INFO::_csCgiList );
for (int i = 0; !bListEmpty && (i < 5); i++)
{
//
// Wait for all threads to complete to avoid
// shutdown timeout problem.
//
Sleep(1000);
EnterCriticalSection( &CGI_INFO::_csCgiList );
if (IsListEmpty(&CGI_INFO::_CgiListHead))
{
bListEmpty = TRUE;
}
LeaveCriticalSection( &CGI_INFO::_csCgiList );
}
}
} // KillCGIProcess
VOID
KillCGIInstanceProcs(
W3_SERVER_INSTANCE *pw3siInstance
)
/*++
Routine Description:
Kill all CGI process
Arguments:
None
Return Value:
Nothing
--*/
{
CGI_INFO* pCgi;
if ( fCGIInitialized )
{
//
// Kill all outstanding process
//
EnterCriticalSection( &CGI_INFO::_csCgiList );
LIST_ENTRY* pEntry;
for ( pEntry = CGI_INFO::_CgiListHead.Flink;
pEntry != &CGI_INFO::_CgiListHead ;
pEntry = pEntry->Flink )
{
pCgi = CONTAINING_RECORD( pEntry,
CGI_INFO,
CGI_INFO::_CgiListEntry );
DBG_ASSERT(pCgi->_pExec);
DBG_ASSERT(pCgi->_pExec->_pRequest);
if ( (pw3siInstance == pCgi->_pExec->_pRequest->QueryW3Instance()) &&
pCgi->_hProcess )
{
CGITerminateProcess( pCgi->_hProcess );
}
}
LeaveCriticalSection( &CGI_INFO::_csCgiList );
}
} // KillCGIProcess
BOOL
HTTP_REQUEST::ProcessGateway(
EXEC_DESCRIPTOR * pExec,
BOOL * pfHandled,
BOOL * pfFinished,
BOOL fTrusted
)
/*++
Routine Description:
Prepares for either a CGI call
If the .exe or .dll isn't found, then *pfHandled will be set
to FALSE and the request will be processed again with the
assumption we just happenned to find a directory with a
trailing .exe or .dll.
Arguments:
pExec - Execution descriptor block
pfHandled - Indicates if the request was a gateway request
pfFinished - Indicates no further processing is required
fTrusted - Can this app be trusted to process things on a read only
vroot
Return Value:
TRUE if successful, FALSE on error
--*/
{
BOOL fRet = TRUE;
TCHAR * pch;
CHAR tmpStr[MAX_PATH];
STR strWorkingDir(tmpStr,MAX_PATH);
DWORD dwMask;
INT cStr;
BOOL fChild = pExec->IsChild();
DWORD dwAttr;
DWORD err;
*pfHandled = FALSE;
*pfFinished = FALSE; // ProcessBGI may reset this
if ( !VrootAccessCheck( pExec->_pMetaData, FILE_GENERIC_EXECUTE ) )
{
DBGPRINTF(( DBG_CONTEXT, "ACESS_DENIED: User \"%s\" doesn't have EXECUTE permissions for CGI %s\n",
_strUserName.QueryStr(), pExec->_pstrPhysicalPath->QueryStr()));
SetDeniedFlags( SF_DENIED_RESOURCE );
return FALSE;
}
if ( !pExec->_pstrPathInfo->Unescape() )
{
return FALSE;
}
//
// If this isn't a trusted app, make sure the user has execute on
// this virtual root
//
if ( !(fTrusted && IS_ACCESS_ALLOWED2(pExec, SCRIPT))
&&
!IS_ACCESS_ALLOWED2(pExec, EXECUTE) )
{
*pfHandled = TRUE;
if ( fChild )
{
SetLastError( ERROR_INVALID_FLAGS );
return FALSE;
}
else
{
DBGPRINTF(( DBG_CONTEXT, "ACCESS_DENIED: No EXECUTE permissions on Vroot \"%s\" for CGI %s\n",
pExec->_pMetaData->QueryVrPath()->QueryStr(), pExec->_pstrPhysicalPath->QueryStr()));
Disconnect( HT_FORBIDDEN, IDS_EXECUTE_ACCESS_DENIED, FALSE, pfFinished );
return TRUE;
}
}
//
// We need to calculate the number of characters that comprise the
// working directory for the script path (which is the web root)
//
if ( !LookupVirtualRoot( pExec->_pstrPhysicalPath,
pExec->_pstrURL->QueryStr(),
pExec->_pstrURL->QueryCCH(),
NULL,
NULL,
&dwMask ))
{
return FALSE;
}
LPSTR pszWorkingDir = strrchr( pExec->_pstrPhysicalPath->QueryStr(), '\\' );
if ( pszWorkingDir == NULL )
{
return FALSE;
}
if ( !strWorkingDir.Copy( pExec->_pstrPhysicalPath->QueryStr(),
DIFF( pszWorkingDir -
pExec->_pstrPhysicalPath->QueryStr() ) + 1))
{
return FALSE;
}
//
// Keep-alive not supported for CGI
//
if ( !fChild )
{
SetKeepConn( FALSE );
}
//
// If this was a mapped script extension expand any parameters
//
if ( !pExec->_pstrGatewayImage->IsEmpty() )
{
CHAR tmpStr[MAX_PATH];
CHAR tmpStr2[1024];
STR strDecodedParams(tmpStr,MAX_PATH);
STR strCmdLine(tmpStr2,1024);
if ( !SetupCmdLine( &strDecodedParams,
*(pExec->_pstrURLParams) ) ||
!strCmdLine.Resize( pExec->_pstrGatewayImage->QueryCB() +
pExec->_pstrPhysicalPath->QueryCB() +
strDecodedParams.QueryCB()))
{
return FALSE;
}
if ( IsCmdExe( pExec->_pstrGatewayImage->QueryStr() ))
{
//
// Make sure the path to the file exists if we're running
// the command interpreter
//
if ( !pExec->ImpersonateUser() )
{
return FALSE;
}
dwAttr = GetFileAttributes( pExec->_pstrPhysicalPath->QueryStr() );
err = GetLastError();
pExec->RevertUser();
if ( dwAttr == 0xffffffff )
{
DBGPRINTF(( DBG_CONTEXT,
"[ProcessGateway] Error %d openning batch file %s\n",
err,
pExec->_pstrPhysicalPath->QueryStr() ) );
if ( !fChild &&
( (err == ERROR_FILE_NOT_FOUND) ||
(err == ERROR_PATH_NOT_FOUND) ||
(err == ERROR_INVALID_NAME) ) )
{
SetState( HTR_DONE, HT_NOT_FOUND, GetLastError() );
Disconnect( HT_NOT_FOUND, NO_ERROR, FALSE, pfFinished );
*pfHandled = TRUE;
return TRUE;
}
return FALSE;
}
}
cStr = wsprintf( strCmdLine.QueryStr(),
pExec->_pstrGatewayImage->QueryStr(),
pExec->_pstrPhysicalPath->QueryStr(),
strDecodedParams.QueryStr() );
strCmdLine.SetLen( cStr );
fRet = ProcessCGI( pExec,
NULL,
&strWorkingDir,
pfHandled,
pfFinished,
&strCmdLine );
}
else
{
fRet = ProcessCGI( pExec,
pExec->_pstrPhysicalPath,
&strWorkingDir,
pfHandled,
pfFinished );
}
return fRet;
}
/********************************************************************/
BOOL
HTTP_REQUEST::ProcessCGI(
EXEC_DESCRIPTOR * pExec,
const STR * pstrPath,
const STR * pstrWorkingDir,
BOOL * pfHandled,
BOOL * pfFinished,
STR * pstrCmdLine
)
/*++
Routine Description:
Processes a CGI client request
Arguments:
pExec - Execution descriptor block
pstrPath - Fully qualified path to executable (or NULL if the module
is contained in pstrCmdLine)
strWorkingDir - Working directory for spawned process
(generally the web root)
pfHandled - Set to TRUE if no further processing is needed
pfFinished - Set to TRUE if no further I/O operation for this request
pstrCmdLine - Optional command line to use instead of the default
Return Value:
TRUE if successful, FALSE on error
--*/
{
STARTUPINFO startupinfo;
PROCESS_INFORMATION processinfo;
UCHAR tmpBuffer[1500];
BUFFER buffEnv(tmpBuffer,1500);
BOOL fRet = FALSE;
CGI_INFO * pCGIInfo = NULL;
DWORD dwThreadId;
HANDLE hThread;
CHAR tmpStr[MAX_PATH];
STR strCmdLine(tmpStr,MAX_PATH);
DWORD dwFlags = DETACHED_PROCESS;
BOOL fChild = pExec->IsChild();
BOOL fIsCmdExe;
*pfHandled = TRUE;
//
// Note we move the module name to the command line so argv
// comes out correctly
//
if ( pstrPath != NULL )
{
if ( !strCmdLine.Copy( "\"", sizeof("\"")-1 ) ||
!strCmdLine.Append( pstrPath->QueryStr() ) ||
!strCmdLine.Append( "\" ", sizeof("\" ")-1 ))
{
return FALSE;
}
}
if ( !SetupChildEnv( pExec,
&buffEnv ) ||
!SetupCmdLine( &strCmdLine,
*(pExec->_pstrURLParams) ))
{
return FALSE;
}
pstrPath = NULL;
//
// If a command line wasn't supplied, then use the default command line
//
if ( !pstrCmdLine )
pstrCmdLine = &strCmdLine;
//
// Check to see if we're spawning cmd.exe, if so, refuse the request if
// there are any special shell characters. Note we do the check here
// so that the command line has been fully unescaped
//
if ( !fAllowSpecialCharsInShell ||
ISUNC(pstrCmdLine->QueryStr()) )
{
if ( fIsCmdExe = IsCmdExe( pstrCmdLine->QueryStr() ) )
{
//
// if invoking cmd.exe for a UNC script then don't set the working directory.
// otherwise cmd.exe will complains about working dir on UNC being not
// supported, which will destroy HTTP headers.
//
if ( ISUNC(pstrCmdLine->QueryStr()) )
{
pstrWorkingDir = NULL;
}
}
}
if ( !fAllowSpecialCharsInShell )
{
DWORD i;
if ( fIsCmdExe )
{
//
// We'll either match one of the characters or the '\0'
//
i = strcspn( pstrCmdLine->QueryStr(), "&|(,;%<>" );
if ( pstrCmdLine->QueryStr()[i] )
{
DBGPRINTF(( DBG_CONTEXT,
"[ProcessCGI] Refusing request for command shell due "
" to special characters\n" ));
SetLastError( ERROR_INVALID_PARAMETER );
return FALSE;
}
}
}
//
// See if CPU Throttling has this CPU stopped.
//
if (_pW3Instance->AreProcsCPUStopped() && pExec->_pMetaData->QueryJobCGIEnabled()) {
SetLastError(ERROR_NOT_ENOUGH_QUOTA);
fRet = FALSE;
goto Exit;
}
//
// Setup the pipes information
//
pCGIInfo = new CGI_INFO( this );
if ( !pCGIInfo )
{
SetLastError( ERROR_NOT_ENOUGH_MEMORY );
goto Exit;
}
pCGIInfo->_pExec = pExec;
ZeroMemory( &startupinfo, sizeof(startupinfo) );
startupinfo.cb = sizeof(startupinfo);
EnterCriticalSection( &CGI_INFO::_csCgiList );
InsertHeadList( &CGI_INFO::_CgiListHead,
&pCGIInfo->_CgiListEntry );
LeaveCriticalSection( &CGI_INFO::_csCgiList );
//
// We specify an unnamed desktop so a new windowstation will be created
// in the context of the calling user
//
startupinfo.lpDesktop = "";
if ( !SetupChildPipes( &startupinfo,
&pCGIInfo->_hStdIn,
&pCGIInfo->_hStdOut ) )
{
IF_DEBUG( CGI )
{
DBGPRINTF((DBG_CONTEXT,
"[ProcessCGI] Failed to create child pipes, error %d",
GetLastError() ));
}
goto Exit;
}
if ( pExec->_pMetaData->QueryCreateProcessNewConsole() )
{
dwFlags = CREATE_NEW_CONSOLE;
}
//////////////////////////////////////////////////////////////
//
// Allow control over whether CreateProcess is called instead of
// CreateProcessAsUser. Running the services as a windows app
// works around the security problem spawning executables
//
// Note this code block is outside the impersonation block
//
//////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////
//
// Spawn the process and close the handles since we don't need them
//
IF_DEBUG( CGI )
{
DBGPRINTF(( DBG_CONTEXT,
"[ProcessCGI] Creating process, path = %s, cmdline = %s\n",
(pstrPath ? pstrPath->QueryStr() : "NULL"),
pstrCmdLine->QueryStr() ));
}
if (pExec->_pMetaData->QueryJobCGIEnabled())
{
dwFlags |= CREATE_SUSPENDED;
}
if ( !pExec->_pMetaData->QueryCreateProcessAsUser() ||
QuerySingleAccessToken() )
{
if ( !pExec->ImpersonateUser() )
{
goto Exit;
}
fRet = CreateProcess( (pstrPath ? pstrPath->QueryStr() : NULL),
pstrCmdLine->QueryStr(),
NULL, // Process security
NULL, // Thread security
TRUE, // Inherit handles
dwFlags,
buffEnv.QueryPtr(),
pstrWorkingDir ? pstrWorkingDir->QueryStr() :
NULL,
&startupinfo,
&processinfo );
pExec->RevertUser();
}
else
{
HANDLE hDelete = NULL;
HANDLE hToken = pExec->QueryPrimaryHandle( &hDelete );
if ( !ImpersonateLoggedOnUser( hToken ) )
{
DBGPRINTF(( DBG_CONTEXT,
"[ProcessCGI] ImpersonateLoggedOnUser failed, error %lx\n",
GetLastError() ));
if ( hDelete )
{
TCP_REQUIRE( CloseHandle( hDelete ));
}
goto Exit;
}
fRet = CreateProcessAsUser( hToken,
(pstrPath ? pstrPath->QueryStr() : NULL),
pstrCmdLine->QueryStr(),
NULL, // Process security
NULL, // Thread security
TRUE, // Inherit handles
dwFlags,
buffEnv.QueryPtr(),
pstrWorkingDir ? pstrWorkingDir->QueryStr() :
NULL,
&startupinfo,
&processinfo );
TCP_REQUIRE( RevertToSelf() );
if ( hDelete )
{
TCP_REQUIRE( CloseHandle( hDelete ) );
}
}
if ((fRet) && (pExec->_pMetaData->QueryJobCGIEnabled()))
{
DBG_ASSERT(_pW3Instance != NULL);
DBG_REQUIRE(_pW3Instance->AddProcessToJob(processinfo.hProcess, FALSE) == ERROR_SUCCESS);
if (ResumeThread(processinfo.hThread) == 0xFFFFFFFF)
{
fRet = FALSE;
TerminateProcess(processinfo.hThread,
GetLastError());
TCP_REQUIRE( CloseHandle( processinfo.hProcess ));
}
}
TCP_REQUIRE( CloseHandle( startupinfo.hStdOutput ));
TCP_REQUIRE( CloseHandle( startupinfo.hStdInput ));
if ( !fRet )
{
DBGPRINTF((DBG_CONTEXT,
"[ProcessCGI] Create process failed, error %d, exe = %s, cmd line = %s\n",
GetLastError(),
(pstrPath ? pstrPath->QueryStr() : "null"),
(pstrCmdLine ? pstrCmdLine->QueryStr() : "null") ));
goto Exit;
}
QueryW3StatsObj()->IncrTotalCGIRequests();
TCP_REQUIRE( CloseHandle( processinfo.hThread ));
DBG_ASSERT( startupinfo.hStdError == startupinfo.hStdOutput);
//
// Save the process handle in case we need to terminate it later on
//
pCGIInfo->_hProcess = processinfo.hProcess;
//
// Before we start the CGI thread, set our new state
//
SetState( HTR_CGI );
if ( QueryW3Instance()->IsUsePoolThreadForCGI() )
{
BOOL fIncPoolThread = FALSE;
//
// To maintain IIS 3.0 compatible behavior, allow 10 CGI threads
// on a single proc machine, since IIS 3.0 by default had 10 ATQ
// threads per processor.
// Note that on multi-proc machines, you will end up with more
// than 10 threads (specifically, you get a boost in the max thread
// count of IIS3_MIN_CGI - ATQ_REG_DEF_PER_PROCESSOR_ATQ_THREADS).
//
if ( InterlockedIncrement( &g_cMinCGIs ) <=
( IIS3_MIN_CGI - ATQ_REG_DEF_PER_PROCESSOR_ATQ_THREADS ) )
{
fIncPoolThread = TRUE;
AtqSetInfo( AtqIncMaxPoolThreads, 0 );
}
//
// Call the CGI processor directly.
//
pCGIInfo->_fServerPoolThread = TRUE;
CGIThread( pCGIInfo );
if ( fIncPoolThread )
{
AtqSetInfo( AtqDecMaxPoolThreads, 0 );
}
InterlockedDecrement( &g_cMinCGIs );
}
else
{
//
// Create a thread to handle IO with the child process
//
// Child execs don't need referencing since they are executed
// synchronously
if ( !fChild )
{
Reference();
}
if ( !(hThread = CreateThread( NULL,
0,
(LPTHREAD_START_ROUTINE) CGIThread,
pCGIInfo,
0,
&dwThreadId )))
{
if ( !fChild )
{
Dereference();
}
goto Exit;
}
//
// If this is a child CGI, we must wait for completion.
// Therefore, wait indefinitely on the CGIThread
//
if ( fChild )
{
TCP_REQUIRE( WaitForSingleObject( hThread, INFINITE ) );
}
//
// We don't use the thread handle so free the resource
//
TCP_REQUIRE( CloseHandle( hThread ));
}
fRet = TRUE;
Exit:
if ( !fRet )
{
DWORD err = GetLastError();
if (pCGIInfo != NULL)
{
EnterCriticalSection( &CGI_INFO::_csCgiList );
RemoveEntryList( &pCGIInfo->_CgiListEntry );
LeaveCriticalSection( &CGI_INFO::_csCgiList );
delete pCGIInfo;
}
if ( !fChild )
{
if ( err == ERROR_ACCESS_DENIED )
{
SetDeniedFlags( SF_DENIED_RESOURCE );
}
else if ( err == ERROR_FILE_NOT_FOUND ||
err == ERROR_PATH_NOT_FOUND )
{
SetState( HTR_DONE, HT_NOT_FOUND, err );
Disconnect( HT_NOT_FOUND, NO_ERROR, FALSE, pfFinished );
fRet = TRUE;
}
else if ( err == ERROR_MORE_DATA ||
( err == ERROR_INVALID_PARAMETER &&
pstrCmdLine->QueryCB() > CGI_COMMAND_LINE_MAXCB ) )
{
// These errors are returned by CreateProcess[AsUser] if
// the query string is too long
SetState( HTR_DONE, HT_URL_TOO_LONG, err );
Disconnect( HT_URL_TOO_LONG, NO_ERROR, FALSE, pfFinished );
fRet = TRUE;
}
else if (err == ERROR_NOT_ENOUGH_QUOTA) {
SetState( HTR_DONE, HT_SVC_UNAVAILABLE, ERROR_NOT_ENOUGH_QUOTA );
Disconnect( HT_SVC_UNAVAILABLE, IDS_SITE_RESOURCE_BLOCKED, FALSE, pfFinished );
fRet = TRUE;
}
}
}
return fRet;
}
/*******************************************************************
NAME: SetupChildPipes
SYNOPSIS: Creates/duplicates pipes for redirecting stdin and
stdout to a child process
ENTRY: pstartupinfo - pointer to startup info structure, receives
child stdin and stdout handles
phParentIn - Pipe to use for parent reading
phParenOut - Pipe to use for parent writing
RETURNS: TRUE if successful, FALSE on failure
HISTORY:
Johnl 22-Sep-1994 Created
********************************************************************/
BOOL SetupChildPipes( STARTUPINFO * pstartupinfo,
HANDLE * phParentIn,
HANDLE * phParentOut )
{
SECURITY_ATTRIBUTES sa;
*phParentIn = NULL;
*phParentOut = NULL;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
pstartupinfo->dwFlags = STARTF_USESTDHANDLES;
//
// Create the pipes then mark them as not inheritted in the
// DuplicateHandle to prevent handle leaks
//
if ( !CreatePipe( phParentIn,
&pstartupinfo->hStdOutput,
&sa,
0 ) ||
!DuplicateHandle( GetCurrentProcess(),
*phParentIn,
GetCurrentProcess(),
phParentIn,
0,
FALSE,
DUPLICATE_SAME_ACCESS |
DUPLICATE_CLOSE_SOURCE) ||
!CreatePipe( &pstartupinfo->hStdInput,
phParentOut,
&sa,
0 ) ||
!DuplicateHandle( GetCurrentProcess(),
*phParentOut,
GetCurrentProcess(),
phParentOut,
0,
FALSE,
DUPLICATE_SAME_ACCESS |
DUPLICATE_CLOSE_SOURCE ))
{
goto ErrorExit;
}
//
// Stdout and Stderror will use the same pipe. If clients tend
// to close stderr, then we'll have to duplicate the handle
//
pstartupinfo->hStdError = pstartupinfo->hStdOutput;
IF_DEBUG ( CGI )
{
DBGPRINTF((DBG_CONTEXT,
"[SetupChildPipes] Parent In = %x, Parent Out = %x, Child In = %x, Child Out = %x\n",
*phParentIn,
*phParentOut,
pstartupinfo->hStdInput,
pstartupinfo->hStdOutput));
}
return TRUE;
ErrorExit:
IF_DEBUG( CGI )
{
DBGPRINTF((DBG_CONTEXT,
"[SetupChildPipes] Failed with error %d\n",
GetLastError()));
}
if ( *phParentIn )
{
TCP_REQUIRE( CloseHandle( *phParentIn ));
*phParentIn = NULL;
}
if ( *phParentOut )
{
TCP_REQUIRE( CloseHandle( *phParentOut ));
*phParentOut = NULL;
}
return FALSE;
}
/*******************************************************************
NAME: SetupChildEnv
SYNOPSIS: Based on the passed pRequest, builds a CGI environment block
ENTRY: pExec - Execution Descriptor Block
pBuff - Buffer to receive environment block
RETURNS: TRUE if successful, FALSE on failure
HISTORY:
Johnl 22-Sep-1994 Created
********************************************************************/
BOOL SetupChildEnv( EXEC_DESCRIPTOR * pExec,
BUFFER * pBuff )
{
TCHAR * pch;
TCHAR * pchtmp;
CHAR tmpStr[1024];
STR strVal(tmpStr,1024);
UINT cchCurrentPos = 0; // Points to '\0' in buffer
UINT cchName, cchValue;
UINT cbNeeded;
BOOL fChild = pExec->IsChild();
HTTP_REQUEST * pRequest = pExec->_pRequest;
int i = 0;
//
// Build the environment block for CGI
//
while ( g_pEnvEntries[i].m_pszName != NULL )
{
//
// Check if this is a copy entry from process environment
//
if ( g_pEnvEntries[i].m_cchToCopy )
{
if ( !pBuff->Resize( (cchCurrentPos + g_pEnvEntries[i].m_cchToCopy)
* sizeof(TCHAR) ) )
{
return FALSE;
}
pch = (TCHAR *) pBuff->QueryPtr();
memcpy( pch + cchCurrentPos,
g_pEnvEntries[i].m_pszName,
g_pEnvEntries[i].m_cchToCopy );
cchCurrentPos += g_pEnvEntries[i].m_cchToCopy;
++i;
continue;
}
//
// The NULL string means we're adding all of
// the HTTP header fields which requires a little
// bit of special processing
//
if ( !*g_pEnvEntries[i].m_pszName )
{
pch = "ALL_HTTP";
}
else
{
pch = g_pEnvEntries[i].m_pszName;
}
if ( !strcmp( pch, "PATH_INFO" ) )
{
if ( !strVal.Copy( pExec->_pstrPathInfo->QueryStr() ) )
{
return FALSE;
}
}
else if ( !strcmp( pch, "PATH_TRANSLATED" ) )
{
if ( !pRequest->LookupVirtualRoot( &strVal,
pExec->_pstrPathInfo->QueryStr(),
pExec->_pstrPathInfo->QueryCCH(),
NULL,
NULL,
NULL,
NULL,
FALSE,
pExec->_pstrPathInfo->QueryCCH() ?
&pExec->_pPathInfoMetaData : NULL,
pExec->_pstrPathInfo->QueryCCH() ?
&pExec->_pPathInfoURIBlob : NULL ) )
{
return FALSE;
}
}
else if ( fChild && !strcmp( pch, "QUERY_STRING" ) )
{
if ( !strVal.Copy( pExec->_pstrURLParams->QueryStr() ) )
{
return FALSE;
}
}
else
{
if ( !pRequest->GetInfo( pch, &strVal ) )
{
return FALSE;
}
}
cchName = _tcslen( g_pEnvEntries[i].m_pszName );
cchValue = strVal.QueryCCH();
//
// We need space for the terminating '\0' and the '='
//
cbNeeded = ( cchName + cchValue + 1 + 1) * sizeof(TCHAR);
if ( !pBuff->Resize( cchCurrentPos * sizeof(TCHAR) + cbNeeded,
512 ))
{
return FALSE;
}
//
// Replace the '\n' with a '\0' as needed
// for the HTTP headers
//
if ( !*g_pEnvEntries[i].m_pszName )
{
pchtmp = strVal.QueryStr();
//
// Convert the first ':' of each header to to an '=' for the
// environment table
//
while ( pchtmp = strchr( pchtmp, ':' ))
{
*pchtmp = '=';
if ( !(pchtmp = strchr( pchtmp, '\n' )))
{
break;
}
}
pchtmp = strVal.QueryStr();
while ( pchtmp = strchr( pchtmp+1, '\n' ))
{
*pchtmp = '\0';
}
}
pch = (TCHAR *) pBuff->QueryPtr();
if ( *g_pEnvEntries[i].m_pszName )
{
if ( strVal.QueryStr()[0] )
{
memcpy( pch + cchCurrentPos,
g_pEnvEntries[i].m_pszName,
cchName * sizeof(TCHAR));
*(pch + cchCurrentPos + cchName) = '=';
memcpy( pch + cchCurrentPos + cchName + 1,
strVal.QueryStr(),
(cchValue + 1) * sizeof(TCHAR));
cchCurrentPos += cchName + cchValue + 1 + 1;
}
}
else
{
CopyMemory(
pch + cchCurrentPos + cchName,
strVal.QueryStr(),
(cchValue + 1) * sizeof(TCHAR));
cchCurrentPos += cchName + cchValue;
}
i++;
}
//
// Add a '\0' terminator to the environment list
//
if ( !pBuff->Resize( (cchCurrentPos + 1) * sizeof(TCHAR)))
{
return FALSE;
}
*((TCHAR *) pBuff->QueryPtr() + cchCurrentPos) = TEXT('\0');
return TRUE;
}
/*******************************************************************
NAME: SetupCmdLine
SYNOPSIS: Sets up a CGI command line
ENTRY: pstrCmdLine - Receives command line
strParams - Parameters following "?" in URL
HISTORY:
Johnl 04-Oct-1994 Created
********************************************************************/
BOOL
SetupCmdLine(
STR * pstrCmdLine,
const STR & strParams
)
{
TCHAR * pch;
//
// If an unencoded "=" is found, don't use the command line
// (some weird CGI rule)
//
if ( _tcschr( strParams.QueryStr(),
TEXT('=') ))
{
return TRUE;
}
STACK_STR (strDecodedParams, 256);
//
// Replace "+" with spaces and decode any hex escapes
//
if ( !strDecodedParams.Copy( strParams ) )
{
return FALSE;
}
while ( pch = _tcschr( strDecodedParams.QueryStr(),
TEXT('+') ))
{
*pch = TEXT(' ');
pch++;
}
if (!strDecodedParams.Unescape() ||
!pstrCmdLine->Append(strDecodedParams))
{
return FALSE;
}
return TRUE;
} // SetupCmdLine
/*******************************************************************
NAME: CGIThread
SYNOPSIS: Sends any gateway data to the scripts stdin and forwards
the script's stdout to the client's socket
ENTRY: Param - Pointer to CGI_INFO structure
HISTORY:
Johnl 22-Sep-1994 Created
********************************************************************/
DWORD CGIThread( PVOID Param )
{
CGI_INFO * pCGIInfo = (CGI_INFO *) Param;
HTTP_REQUEST * pRequest = pCGIInfo->_pRequest;
EXEC_DESCRIPTOR * pExec = pCGIInfo->_pExec;
BYTE buff[2048];
DWORD cbWritten;
DWORD cbRead;
DWORD cbSent;
DWORD dwExitCode;
DWORD err;
BOOL fReadHeaders = FALSE;
BOOL fChild = pExec->IsChild();
BOOL fNoHeaders = pExec->NoHeaders();
BOOL fRedirectOnly = pExec->RedirectOnly();
BOOL fDone = FALSE;
BOOL fSkipDisconnect;
BOOL fRet = TRUE;
DWORD dwHttpStatus = HT_OK;
DWORD msScriptTimeout = pRequest->QueryMetaData()->QueryScriptTimeout()
* 1000;
STACK_STR(strTemp, 128);
STACK_STR(strResponse, 512);
CHAR ach[17];
CHAR *pszCRLF;
DWORD dwCRLFSize;
BOOL fExitCodeProcess;
DWORD dwCookie = 0;
IF_DEBUG( CGI )
{
DBGPRINTF((DBG_CONTEXT,
"[CGIThread] Entered, hstdin %x, hstdout = %x\n",
pCGIInfo->_hStdIn,
pCGIInfo->_hStdOut));
}
pRequest->SetLogStatus( HT_OK, NO_ERROR );
//
// Update the statistics counters
//
pRequest->QueryW3StatsObj()->IncrCGIRequests();
//
// Schedule a callback to kill the process if he doesn't die
// in a timely manner
//
pCGIInfo->_dwSchedCookie = ScheduleWorkItem( CGITerminateProcess,
pCGIInfo->_hProcess,
msScriptTimeout );
if ( !pCGIInfo->_dwSchedCookie )
{
DBGPRINTF(( DBG_CONTEXT,
"[CGI_THREAD] ScheduleWorkItem failed, error %d\n",
GetLastError() ));
}
//
// First we have to write any additional data to the program's stdin
//
if ( pRequest->QueryEntityBodyCB() )
{
DWORD cbNextRead;
DWORD cbLeft = 0;
IF_DEBUG( CGI )
{
DBGPRINTF((DBG_CONTEXT,
"[CGIThread] Writing %d bytes to child's stdin\n",
pRequest->QueryEntityBodyCB()));
}
if ( !::WriteFile( pCGIInfo->_hStdOut,
pRequest->QueryEntityBody(),
pRequest->QueryEntityBodyCB(),
&cbWritten,
NULL ))
{
DBGPRINTF((DBG_CONTEXT,
"[CGI_THREAD] WriteFile failed, error %d\n",
GetLastError()));
}
if ( cbWritten != pRequest->QueryEntityBodyCB() )
{
DBGPRINTF((DBG_CONTEXT,
"[CGI_THREAD] %d bytes written of %d bytes\n",
cbWritten,
pRequest->QueryEntityBodyCB()));
}
//
// Now stream any unread data to the CGI application but
// watch out for the case where we get more data then is
// indicated by the Content-Length
//
if ( pRequest->QueryEntityBodyCB() < pRequest->QueryClientContentLength())
{
cbLeft = pRequest->QueryClientContentLength() -
pRequest->QueryEntityBodyCB();
}
while ( cbLeft )
{
cbNextRead = min( cbLeft,
pRequest->QueryClientReqBuff()->QuerySize() );
if ( !pRequest->ReadFile( pRequest->QueryClientRequest(),
cbNextRead,
&cbRead,
IO_FLAG_SYNC ) ||
!cbRead )
{
DBGPRINTF(( DBG_CONTEXT,
"[CGI_THREAD] Error reading gateway data (%d)\n",
GetLastError() ));
fRet = FALSE;
break;
}
cbLeft -= cbRead;
pRequest->AddTotalEntityBodyCB( cbRead );
if ( !::WriteFile( pCGIInfo->_hStdOut,
pRequest->QueryClientRequest(),
cbRead,
&cbWritten,
NULL ))
{
fRet = FALSE;
break;
}
}
}
if ( !fRet )
{
//
// If an error occurred during the client read or write, we let the CGI
// application continue
//
DBGPRINTF((DBG_CONTEXT,
"[CGI_THREAD] Gateway ReadFile or CGI WriteFile failed, error %d\n",
GetLastError()));
}
//
// Now wait for any data the child sends to its stdout or for the
// process to exit
//
//
// Handle input from child
//
while (TRUE)
{
fRet = ::ReadFile( pCGIInfo->_hStdIn,
buff,
sizeof(buff),
&cbRead,
NULL );
if ( !fRet )
{
err = GetLastError();
if ( err == ERROR_BROKEN_PIPE )
{
break;
}
IF_DEBUG( CGI )
{
DBGPRINTF((DBG_CONTEXT,
"[CGI_THREAD] ReadFile from child stdout failed, error %d, _hStdIn = %x\n",
GetLastError(),
pCGIInfo->_hStdIn));
}
pRequest->SetLogStatus( 500, err );
break;
}
IF_DEBUG( CGI )
{
DBGPRINTF((DBG_CONTEXT,
"[CGI_THREAD] ReadFile read %d bytes\n",
cbRead));
}
//
// If no bytes were read, assume the file has been closed so
// get out
//
if ( !cbRead )
{
break;
}
//
// The CGI script can specify headers to include in the
// response. Wait till we receive all of the headers.
//
if ( !fReadHeaders && !pExec->IsNPH() )
{
if ( !fNoHeaders)
{
if ( !ProcessCGIInput( pCGIInfo,
buff,
cbRead,
&fReadHeaders,
&fDone,
&fSkipDisconnect,
&dwHttpStatus ))
{
DBGPRINTF((DBG_CONTEXT,
"[CGIThread] ProcessCGIInput failed with error %d\n",
GetLastError()));
if ( fChild )
{
goto SkipDisconnect;
}
else
{
goto Disconnect;
}
}
if ( fSkipDisconnect )
{
goto SkipDisconnect;
}
if ( fDone )
{
if ( fChild )
{
goto SkipDisconnect;
}
else
{
goto Disconnect;
}
}
}
else
{
BYTE * pbExtraData;
DWORD cbRemainingData;
pbExtraData = ScanForTerminator( (CHAR*) buff );
if ( pbExtraData != NULL )
{
fReadHeaders = TRUE;
cbRemainingData = cbRead - DIFF( pbExtraData - buff );
if ( HTV_HEAD != pRequest->QueryVerb() &&
!pRequest->WriteFile( pbExtraData,
cbRemainingData,
&cbSent,
IO_FLAG_SYNC ) )
{
pRequest->SetLogStatus( HT_SERVER_ERROR,
GetLastError() );
break;
}
}
}
//
// Either we are waiting for the rest of the header or
// we've sent the header and any residual data so wait
// for more data
//
continue;
}
if ( (HTV_HEAD != pRequest->QueryVerb()) &&
!pRequest->WriteFile( buff,
cbRead,
&cbSent,
IO_FLAG_SYNC ))
{
DBGPRINTF((DBG_CONTEXT,
"[CGI_THREAD] WriteFile to socket failed, error %d\n",
GetLastError()));
pRequest->SetLogStatus( 500,
GetLastError() );
break;
}
}
//
// Remove the scheduled timeout callback
//
dwCookie = (DWORD) InterlockedExchange( (LPLONG) &(pCGIInfo->_dwSchedCookie),
0 );
if ( dwCookie != 0 )
{
if ( !RemoveWorkItem( dwCookie ) )
{
DBGPRINTF(( DBG_CONTEXT,
"[CGI_THREAD] Failed to remove scheduled item\n" ));
}
}
//
// If we had to kill the process, or the Job Object killed the process,
// log an event
//
fExitCodeProcess = GetExitCodeProcess( pCGIInfo->_hProcess,
&dwExitCode );
if ( (fExitCodeProcess) &&
(( dwExitCode == CGI_PREMATURE_DEATH_CODE ) ||
( dwExitCode == ERROR_NOT_ENOUGH_QUOTA )))
{
const CHAR * apsz[2];
//
// Log an event and terminate the process
//
DBGPRINTF((DBG_CONTEXT,
"[CGI_THREAD] - CGI Script %s, params %s was killed\n",
pExec->_pstrURL->QueryStr(),
pExec->_pstrURLParams->QueryStr()));
apsz[0] = pExec->_pstrURL->QueryStr();
apsz[1] = pExec->_pstrURLParams->QueryStr();
//
// As it happens, this message is appropriate for Job Object
// CGI Timeout as well as the original timeout
//
g_pInetSvc->LogEvent( W3_EVENT_KILLING_SCRIPT,
2,
apsz,
0 );
dwHttpStatus = HT_BAD_GATEWAY;
//
// If we haven't sent the headers, build up a full response, otherwise
// tack on the message to the end of the current output
//
if ( !fNoHeaders && !fRedirectOnly )
{
DWORD dwTemp;
pRequest->SetLogStatus( HT_BAD_GATEWAY,
ERROR_SERVICE_REQUEST_TIMEOUT );
if ( HTTP_REQ_BASE::BuildStatusLine( &strTemp,
HT_BAD_GATEWAY,
0,
pRequest->QueryURL(),
NULL))
{
if ( pRequest->BuildBaseResponseHeader( &strResponse,
&fDone,
&strTemp,
HTTPH_NO_CUSTOM))
{
strResponse.SetLen( strlen(strResponse.QueryStr()) );
strTemp.SetLen(0);
if (pRequest->CheckCustomError(&strTemp, HT_BAD_GATEWAY,
MD_ERROR_SUB502_TIMEOUT,
&fDone,
&dwTemp))
{
if (fDone)
{
goto SkipDisconnect;
}
pszCRLF = "\r\n";
dwCRLFSize = CRLF_SIZE;
DBG_REQUIRE(strTemp.SetLen(strlen(strTemp.QueryStr())) );
}
else
{
if ( !g_pInetSvc->LoadStr( strTemp,
IDS_CGI_APP_TIMEOUT ))
{
goto Disconnect;
}
dwTemp = strTemp.QueryCB();
pszCRLF = "\r\nContent-Type: text/html\r\n\r\n";
dwCRLFSize = sizeof("\r\nContent-Type: text/html\r\n\r\n") - 1;
}
_itoa( dwTemp, ach, 10 );
DBG_REQUIRE(strResponse.Append("Content-Length: ",
sizeof("Content-Length: ") - 1));
DBG_REQUIRE(strResponse.Append(ach));
DBG_REQUIRE(strResponse.Append(pszCRLF, dwCRLFSize));
if (HTV_HEAD != pRequest->QueryVerb())
{
DBG_REQUIRE( strResponse.Append( strTemp));
}
pRequest->SendHeader( strResponse.QueryStr(),
strResponse.QueryCB(),
IO_FLAG_SYNC,
&fDone );
}
}
}
else
{
if ( g_pInetSvc->LoadStr( strResponse, IDS_CGI_APP_TIMEOUT ))
{
pRequest->WriteFile( strResponse.QueryStr(),
strResponse.QueryCB(),
&cbSent,
IO_FLAG_SYNC );
}
}
}
else if ( !fReadHeaders )
{
//
// If we never finished reading the headers, send a nice message
// to the client
//
if ( !fNoHeaders && !fRedirectOnly && !pExec->IsNPH() )
{
DWORD dwTemp;
CHAR *pszTemp;
dwHttpStatus = HT_BAD_GATEWAY;
pRequest->SetLogStatus( HT_BAD_GATEWAY,
ERROR_SERVICE_REQUEST_TIMEOUT );
if ( HTTP_REQ_BASE::BuildStatusLine( &strTemp,
HT_BAD_GATEWAY,
0,
pRequest->QueryURL(),
NULL))
{
CHAR ach[17];
CHAR *pszCRLF;
DWORD dwCRLFSize;
DWORD dwExtraSize;
if ( pRequest->BuildBaseResponseHeader( &strResponse,
&fDone,
&strTemp,
HTTPH_NO_CUSTOM))
{
DWORD dwCGIHeaderLength;
strResponse.SetLen( strlen(strResponse.QueryStr()) );
strTemp.SetLen(0);
if (pRequest->CheckCustomError(&strTemp, HT_BAD_GATEWAY,
MD_ERROR_SUB502_PREMATURE_EXIT,
&fDone,
&dwTemp))
{
if (fDone)
{
goto SkipDisconnect;
}
pszCRLF = "\r\n";
dwCRLFSize = CRLF_SIZE;
DBG_REQUIRE(strTemp.SetLen(strlen(strTemp.QueryStr())) );
dwExtraSize = strTemp.QueryCB() - dwTemp;
}
else
{
if ( !g_pInetSvc->LoadStr( strTemp, IDS_BAD_CGI_APP ))
{
goto Disconnect;
}
dwTemp = strTemp.QueryCB();
dwExtraSize = 0;
pszCRLF = "\r\nContent-Type: text/html\r\n\r\n";
dwCRLFSize = sizeof("\r\nContent-Type: text/html\r\n\r\n") - 1;
}
dwCGIHeaderLength = strlen((CHAR *)pCGIInfo->_Buff.QueryPtr() );
if (strstr(strTemp.QueryStr(), "%s") != NULL)
{
dwTemp += dwCGIHeaderLength;
dwTemp -= (sizeof("%s") - 1);
}
// Truncate the buffer to 1024, since wsprintf won't do more than that.
if (dwTemp > 1024)
{
dwTemp = 1024;
}
_itoa( dwTemp, ach, 10 );
DBG_REQUIRE(strResponse.Append("Content-Length: ",
sizeof("Content-Length: ") - 1));
DBG_REQUIRE(strResponse.Append(ach));
DBG_REQUIRE(strResponse.Append(pszCRLF, dwCRLFSize));
if (HTV_HEAD != pRequest->QueryVerb())
{
//
// There might be a string substitute pattern in the error
// message. Resize the buffer to handle it, and substitue
// the headers.
//
if (!strResponse.Resize(strResponse.QueryCB() +
dwTemp + dwExtraSize + 1))
{
goto Disconnect;
}
if (!pCGIInfo->_Buff.Resize(dwCGIHeaderLength + 1))
{
goto Disconnect;
}
pszTemp = (CHAR *)pCGIInfo->_Buff.QueryPtr();
pszTemp[dwCGIHeaderLength] = '\0';
wsprintf(strResponse.QueryStr() + strResponse.QueryCB(),
strTemp.QueryStr(), pszTemp);
DBG_REQUIRE(strResponse.SetLen(strResponse.QueryCB() +
dwTemp + dwExtraSize));
}
pRequest->SendHeader( strResponse.QueryStr(),
strResponse.QueryCB(),
IO_FLAG_SYNC,
&fDone );
}
}
}
else
{
//
// We don't want headers and none were found.
// Send whatever CGI output was 'as is'.
//
if ( pCGIInfo->_cbData )
{
pRequest->WriteFile( pCGIInfo->_Buff.QueryPtr(),
pCGIInfo->_cbData,
&cbSent,
IO_FLAG_SYNC );
}
}
}
if ( fChild )
{
goto SkipDisconnect;
}
Disconnect:
DBG_ASSERT( !fChild );
IF_DEBUG ( CGI )
{
DBGPRINTF((DBG_CONTEXT,
"[CGIThread] Exiting thread, Current State = %d, Ref = %d\n",
pRequest->QueryState(),
pRequest->QueryRefCount()));
}
//
// Make sure this request gets logged
//
pRequest->SetState( pRequest->QueryState(),
dwHttpStatus,
NO_ERROR ); // Don't have a good Win32 mapping here
pRequest->WriteLogRecord();
//
// Force a shutdown here so the CGI process exit doesn't cause a
// reset on this socket
//
pRequest->Disconnect( 0, NO_ERROR, TRUE );
SkipDisconnect: // The only time the disconnect is skipped is when
// a CGI app sends a redirect
if ( !pCGIInfo->_fServerPoolThread )
{
//
// Indicate that this Atq context should not be used because this thread
// is about to go away which will cause the AcceptEx IO to be cancelled
//
pRequest->QueryClientConn()->SetAtqReuseContextFlag( FALSE );
//
// Reference is only done if we've created a new thread
//
if ( !fChild )
{
DereferenceConn( pRequest->QueryClientConn() );
}
}
EnterCriticalSection( &CGI_INFO::_csCgiList );
RemoveEntryList( &pCGIInfo->_CgiListEntry );
LeaveCriticalSection( &CGI_INFO::_csCgiList );
delete pCGIInfo;
pRequest->QueryW3StatsObj()->DecrCurrentCGIRequests();
return 0;
}
/*******************************************************************
NAME: ProcessCGIInput
SYNOPSIS: Handles headers the CGI program hands back to the server
ENTRY: pCGIInfo - Pointer to CGI structure
buff - Pointer to data just read
cbRead - Number of bytes read into buff
pfReadHeaders - Set to TRUE after we've finished processing
all of the HTTP headers the CGI script gave us
pfDone - Set to TRUE to indicate no further processing is
needed
pfSkipDisconnect - Set to TRUE to indicate no further
processing is needed and the caller should not call
disconnect
HISTORY:
Johnl 22-Sep-1994 Created
********************************************************************/
BOOL
ProcessCGIInput(
CGI_INFO * pCGIInfo,
BYTE * buff,
DWORD cbRead,
BOOL * pfReadHeaders,
BOOL * pfDone,
BOOL * pfSkipDisconnect,
DWORD * pdwHttpStatus
)
{
PCHAR pchValue;
PCHAR pchField;
BYTE * pbData;
PCHAR pszTail;
DWORD cbData;
DWORD cbSent;
STACK_STR( strContentType, 64 );
STACK_STR( strStatus, 32 );
STACK_STR( strCGIResp, MAX_PATH ); // Contains CGI client headers
BOOL fFoundContentType = FALSE;
BOOL fFoundStatus = FALSE;
HTTP_REQUEST * pRequest = pCGIInfo->_pRequest;
DWORD cbNeeded, cbBaseResp;
BOOL fNoHeaders = pCGIInfo->_pExec->NoHeaders();
BOOL fRedirectOnly = pCGIInfo->_pExec->RedirectOnly();
STACK_STR( strError, 80);
DWORD dwContentLength;
CHAR ach[17];
DWORD dwCLLength;
DBG_ASSERT( cbRead > 0 );
*pfDone = FALSE;
*pfSkipDisconnect = FALSE;
if ( !pCGIInfo->_Buff.Resize( pCGIInfo->_cbData + cbRead,
256 ))
{
return FALSE;
}
CopyMemory( (BYTE *)pCGIInfo->_Buff.QueryPtr() + pCGIInfo->_cbData,
buff,
cbRead );
pCGIInfo->_cbData += cbRead;
//
// The end of CGI headers are marked by a blank line, check to see if
// we've hit that line
//
if ( !CheckForTermination( pfReadHeaders,
&pCGIInfo->_Buff,
pCGIInfo->_cbData,
&pbData,
&cbData,
256 ))
{
return FALSE;
}
if ( !*pfReadHeaders )
{
return TRUE;
}
//
// We've found the end of the headers, process them
//
// if request header contains:
//
// Content-Type: xxxx - Send as the content type
// Location: xxxx - if starts with /, send doc, otherwise send redirect message
// URI: preferred synonym to Location:
// Status: nnn xxx - Send as status code (HTTP/1.0 nnn xxx)
//
// Send other request headers (server, message date, mime version)
//
CHAR pszOutputString[512];
BOOL fDidRedirect = FALSE;
INET_PARSER Parser( (CHAR *) pCGIInfo->_Buff.QueryPtr() );
while ( *(pchField = Parser.QueryToken()) )
{
Parser.SkipTo( ':' );
Parser += 1;
pchValue = Parser.QueryToken();
if ( !fRedirectOnly &&
!::_strnicmp( "Status", pchField, 6 ) )
{
DWORD cbLine = strlen( Parser.QueryLine());
fFoundStatus = TRUE;
*pdwHttpStatus = atoi( pchValue );
if ( !strStatus.Resize( LEN_PSZ_HTTP_VERSION_STR +
cbLine + 4) ||
!strStatus.Copy( !g_ReplyWith11 ? PSZ_HTTP_VERSION_STR :
PSZ_HTTP_VERSION_STR11,
LEN_PSZ_HTTP_VERSION_STR ) ||
!strStatus.Append( Parser.QueryLine(), cbLine ) )
{
return FALSE;
}
// I am safe to assume space is there, because of resize
DBG_ASSERT( strStatus.QueryCB() < (strStatus.QuerySize() - 2));
strStatus.AppendCRLF();
}
else if ( !_strnicmp( "Location", pchField, 8 ) ||
!_strnicmp( "URI", pchField, 3 ))
{
//
// The CGI script is redirecting us to another URL.
// If it begins with a '/', then send it, otherwise
// send a redirect message
//
if ( *pchValue == TEXT('/') && !fRedirectOnly )
{
if ( !pRequest->ReprocessURL( pchValue,
HTV_GET ))
{
return FALSE;
}
*pfSkipDisconnect = TRUE;
*pfDone = TRUE;
return TRUE;
}
DWORD cbLen;
CHAR pszMessageString[256];
cbLen = LoadString( GetModuleHandle( W3_MODULE_NAME ),
IDS_URL_MOVED,
pszMessageString,
256 );
if ( !cbLen )
{
return FALSE;
}
wsprintf( pszOutputString,
pszMessageString,
pchValue );
if ( fRedirectOnly )
{
if ( !pRequest->WriteFile( pszOutputString,
strlen(pszOutputString),
&cbLen,
IO_FLAG_SYNC ) )
{
return FALSE;
}
}
else if ( !strCGIResp.Append( "Location: ", 10 ) ||
!strCGIResp.Append( pchValue ) ||
!strCGIResp.Append( "\r\n", 2 ) ||
!strStatus.Copy( !g_ReplyWith11 ? PSZ_HTTP_VERSION_STR :
PSZ_HTTP_VERSION_STR11,
LEN_PSZ_HTTP_VERSION_STR ) ||
!strStatus.Append( "302 Object Moved\r\n", 18 ) )
{
return FALSE;
}
fDidRedirect = TRUE;
}
else if ( !fRedirectOnly )
{
//
// Copy any other fields the script specified
//
Parser.QueryLine();
if ( !::_strnicmp( "Content-Type", pchField, 12 ))
{
fFoundContentType = TRUE;
if ( !strContentType.Append( pchField ) ||
!strContentType.Append( "\r\n", 2 ))
{
return FALSE;
}
}
else
{
//
// Terminate line
//
if ( !strCGIResp.Append( pchField ) ||
!strCGIResp.Append( "\r\n", 2 ))
{
return FALSE;
}
}
}
Parser.NextLine();
}
//
// If we're ignoring all but redirects, then simply sent the data
// past the headers and we're done.
//
if ( fRedirectOnly )
{
goto SendRemainder;
}
//
// If the CGI script didn't specify a content type, then use
// the default
//
if ( fDidRedirect )
{
if ( !strContentType.Copy( "Content-Type: text/html\r\n", 25 ) )
{
return FALSE;
}
}
else if ( !fFoundContentType )
{
STR str;
// NYI: SelectMimeMapping will yield a string with allocation
// this is a temp string - try to avoid allocs
if ( !strContentType.Append( PSZ_KWD_CONTENT_TYPE,
LEN_PSZ_KWD_CONTENT_TYPE )||
!SelectMimeMapping( &str,
NULL,
pCGIInfo->_pExec->_pMetaData )||
!strContentType.Append( str ) ||
!strContentType.Append( "\r\n", 2 ))
{
return FALSE;
}
}
//
// Combine the CGI specified headers with the regular headers
// the server would send (message date, server ver. etc)
//
if ( !*pdwHttpStatus && !fDidRedirect )
{
*pdwHttpStatus = HT_OK;
}
else
{
if ( *pdwHttpStatus == HT_DENIED )
{
pRequest->SetDeniedFlags( SF_DENIED_APPLICATION );
pRequest->SetAuthenticationRequested( TRUE );
}
}
if ( !pRequest->BuildBaseResponseHeader( pRequest->QueryRespBuf(),
pfDone,
(fFoundStatus || fDidRedirect) ?
&strStatus : NULL,
(*pdwHttpStatus == HT_OK) ?
0 : HTTPH_NO_CUSTOM
))
{
return FALSE;
}
if ( *pfDone )
{
return TRUE;
}
if ( *pdwHttpStatus != HT_OK && !fDidRedirect )
{
DWORD dwSubStatus;
BOOL fErrorDone;
// Some sort of error status, check for a custom error.
fErrorDone = FALSE;
dwSubStatus = pRequest->IsAuthenticationRequested() ?
MD_ERROR_SUB401_APPLICATION : 0;
if (pRequest->CheckCustomError(&strError, *pdwHttpStatus,
dwSubStatus, &fErrorDone,
&dwContentLength,
dwSubStatus == 0 ? TRUE : FALSE))
{
// Had some sort of a custom error. If it's being completely
// handled, bail out now.
if (fErrorDone)
{
*pfSkipDisconnect = TRUE;
return TRUE;
}
// We had a custom error, but it wasn't completely handled. This
// overrides a content-type and any content sent by the CGI script
// itself.
strError.SetLen(strlen(strError.QueryStr()));
cbData = 0;
}
}
cbBaseResp = pRequest->QueryRespBufCB();
if (strError.QueryCB() != 0)
{
_itoa( dwContentLength, ach, 10 );
dwCLLength = strlen(ach);
cbNeeded = cbBaseResp +
strError.QueryCB() +
sizeof("Content-Length: \r\n") - 1 +
dwCLLength +
1; // For trailing NULL.
if ( !pRequest->QueryRespBuf()->Resize( cbNeeded ))
{
return FALSE;
}
pszTail = pRequest->QueryRespBufPtr() + cbBaseResp;
memcpy(pszTail, "Content-Length: ", sizeof("Content-Length: ") - 1);
pszTail += sizeof("Content-Length: ") - 1;
memcpy(pszTail, ach, dwCLLength);
pszTail += dwCLLength;
memcpy(pszTail, "\r\n", CRLF_SIZE);
pszTail += CRLF_SIZE;
memcpy(pszTail, strError.QueryStr(), strError.QueryCB() + 1);
pszTail += strError.QueryCB();
}
else
{
cbNeeded = cbBaseResp +
strContentType.QueryCB() +
strCGIResp.QueryCB() +
sizeof( "\r\n" ); // Include the '\0' in the count
if ( fDidRedirect )
{
cbNeeded += strlen(pszOutputString);
}
if ( !pRequest->QueryRespBuf()->Resize( cbNeeded ))
{
return FALSE;
}
pszTail = pRequest->QueryRespBufPtr() + cbBaseResp;
memcpy( pszTail, strContentType.QueryStr(), strContentType.QueryCB() );
pszTail += strContentType.QueryCB();
memcpy(pszTail, strCGIResp.QueryStr(), strCGIResp.QueryCB());
pszTail += strCGIResp.QueryCB();
memcpy(pszTail, "\r\n", CRLF_SIZE+1);
pszTail += CRLF_SIZE;
if ( fDidRedirect )
{
memcpy(pszTail, pszOutputString, strlen(pszOutputString));
pszTail += strlen(pszOutputString);
}
}
if ( !pRequest->SendHeader( pRequest->QueryRespBufPtr(),
DIFF(pszTail - pRequest->QueryRespBufPtr()),
IO_FLAG_SYNC,
pfDone ))
{
return FALSE;
}
//
// If we had a custom error message, make sure we set the done flag now.
//
if (strError.QueryCB() != 0)
{
*pfDone = TRUE;
}
if ( fDidRedirect )
{
*pfDone = TRUE;
return TRUE;
}
//
// If there was additional data in the buffer, send that out now
//
SendRemainder:
if ( cbData )
{
if ( !pRequest->WriteFile( pbData,
cbData,
&cbSent,
IO_FLAG_SYNC ))
{
return FALSE;
}
}
return TRUE;
}
VOID
WINAPI
CGITerminateProcess(
PVOID pContext
)
/*++
Routine Description:
This function is the callback called by the scheduler thread after the
specified timeout period has elapsed.
Arguments:
pContext - Handle of process to kill
--*/
{
IF_DEBUG( CGI )
{
DBGPRINTF(( DBG_CONTEXT,
"[CGITerminateProcess] - Terminating process handle %x\n",
pContext ));
}
if ( !TerminateProcess( (HANDLE) pContext, CGI_PREMATURE_DEATH_CODE ))
{
DBGPRINTF((DBG_CONTEXT,
"[CGITerminateProcess] - TerminateProcess returned %d\n",
GetLastError()));
}
} // CGITerminateProcess
BOOL
IsCmdExe(
const CHAR * pchPath
)
{
while ( *pchPath )
{
if ( (*pchPath == 'c') || (*pchPath == 'C') )
{
if ( !_strnicmp(pchPath,"cmd.exe",sizeof("cmd.exe") - 1)
|| !_strnicmp(pchPath,"command.com",sizeof("command.com") - 1)
)
{
return TRUE;
}
}
pchPath++;
}
return FALSE;
} // IsCmdExe