/*++

Copyright (c) 1996  Microsoft Corporation

Module Name:

    ssicgi.cxx

Abstract:

    This is CGI code "ripped" from \w3\server\cgi.cxx
    
Author:

    Bilal Alam (t-bilala)       5-June-1996

Revision History:

    See iis\svcs\w3\server\cgi.cxx for prior log

--*/

#include "ssinc.hxx"
#include "ssicgi.hxx"

typedef struct CgiEnvTableEntry_ {
    CHAR* 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[] =
{
    {"AUTH_TYPE",FALSE},
    {"ComSpec",TRUE},
    {"CONTENT_LENGTH",FALSE},
    {"CONTENT_TYPE",FALSE},
    {"GATEWAY_INTERFACE",FALSE},
    {"",FALSE},                   // Means insert all HTTP_ headers here
    {"LOGON_USER",FALSE},
    {"PATH",TRUE},
    {"PATH_INFO",FALSE},
    {"PATH_TRANSLATED",FALSE},
    {"QUERY_STRING",FALSE},
    {"REMOTE_ADDR",FALSE},
    {"REMOTE_HOST",FALSE},
    {"REMOTE_USER",FALSE},
    {"REQUEST_METHOD",FALSE},
    {"SCRIPT_NAME",FALSE},
    {"SERVER_NAME",FALSE},
    {"SERVER_PORT",FALSE},
    {"SERVER_PORT_SECURE",FALSE},
    {"SERVER_PROTOCOL",FALSE},
    {"SERVER_SOFTWARE",FALSE},
    {"SystemRoot",TRUE},
    {"UNMAPPED_REMOTE_USER",FALSE},
    {"windir",TRUE},
    {NULL,FALSE}
};

// 
// Globals
//

BOOL fAllowSpecialCharsInShell = FALSE;
BOOL fCreateProcessAsUser = TRUE;
BOOL fCreateProcessWithNewConsole = FALSE;
BOOL fForwardServerEnvironmentBlock = TRUE;
LPSTR  g_pszIisEnv = NULL;
CgiEnvTableEntry *g_pEnvEntries = NULL;
DWORD dwTimeOut = 0;
BOOL fInitialized = FALSE;

class CGI_INFO
{
public:
    CGI_INFO( SSI_REQUEST * pRequest )
        : _pRequest         ( pRequest ),
          _cbData           ( 0 ),
          _hStdOut          ( INVALID_HANDLE_VALUE ),
          _hStdIn           ( INVALID_HANDLE_VALUE ),
          _hProcess         ( INVALID_HANDLE_VALUE ),
          _fShutdown        ( FALSE )
    {
        _hResponseEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
    }

    ~CGI_INFO( VOID )
    {
        if ( _hStdOut != INVALID_HANDLE_VALUE )
        {
            if ( !::CloseHandle( _hStdOut ))
            {
                TCP_PRINT((DBG_CONTEXT,
                          "[~CGI_INFO] CloseHandle failed on StdIn, %d\n",
                           GetLastError()));
            }
        }

        if ( _hStdIn != INVALID_HANDLE_VALUE )
        {
            if ( !::CloseHandle( _hStdIn ))
            {
                TCP_PRINT((DBG_CONTEXT,
                          "[~CGI_INFO] CloseHandle failed on StdIn, %d\n",
                           GetLastError()));
            }
        }

        if ( _hProcess != INVALID_HANDLE_VALUE )
        {
            if ( !::CloseHandle( _hProcess ))
            {
                TCP_PRINT((DBG_CONTEXT,
                          "[~CGI_INFO] CloseHandle failed on Process, %d\n",
                           GetLastError()));
            }
        }
        if ( _hResponseEvent != NULL )
        {
            if ( !::CloseHandle( _hResponseEvent ))
            {
                TCP_PRINT((DBG_CONTEXT,
                          "[~CGI_INFO] CloseHandle failed on Process, %d\n",
                           GetLastError()));
            }
        }
    }

    SSI_REQUEST *   _pRequest;

    //
    //  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;

    //
    //  Event to check for hanging processes
    //  and thread shutdown flag
    //

    HANDLE          _hResponseEvent;
    BOOL            _fShutdown;
};

//
// Prototypes
//

BOOL SetupChildEnv( SSI_REQUEST *  pRequest,
                    BUFFER       * pBuff,
                    const STR *    pstrPathInfo,
                    const STR *    pstrQueryString );

BOOL SetupChildPipes( STARTUPINFO * pstartupinfo,
                      HANDLE      * phParentIn,
                      HANDLE      * phParentOut );

DWORD CGIThread( PVOID Param );

BOOL ProcessCGIInput( CGI_INFO * pCGIInfo,
                      BYTE     * buff,
                      DWORD      cbRead,
                      BOOL     * pfReadHeaders );

BOOL CheckForTermination( BOOL   * pfTerminated,
                          BUFFER * pbuff,
                          UINT     cbData,
                          BYTE * * ppbExtraData,
                          DWORD *  pcbExtraData,
                          UINT     cbReallocSize );
BOOL
FastScanForTerminator(
    CHAR *  pch,
    UINT    cbData
    );

DWORD
InitializeCGI( 
    VOID 
    );

VOID 
TerminateCGI( 
    VOID 
    );

//
// End of prototypes
//

DWORD
ReadRegistryDword(
    IN HKEY         hkey,
    IN LPSTR        pszValueName,
    IN DWORD        dwDefaultValue
)
{
    DWORD  err;
    DWORD  dwBuffer;

    DWORD  cbBuffer = sizeof(dwBuffer);
    DWORD  dwType;

#ifndef CHICAGO
    if ( hkey != NULL )
    {
        err = RegQueryValueExA( hkey,
                               pszValueName,
                               NULL,
                               &dwType,
                               (LPBYTE)&dwBuffer,
                               &cbBuffer );

        if ( ( err == NO_ERROR ) && ( dwType == REG_DWORD ) )
        {
            dwDefaultValue = dwBuffer;
        }
    }
#else
    if ( hkey != NULL )
    {

        err = RegQueryValueEx( hkey,
                               pszValueName,
                               NULL,
                               &dwType,
                               (LPBYTE)&dwBuffer,
                               &cbBuffer );

        if ( ( err == NO_ERROR )  )
        {
            dwDefaultValue = dwBuffer;
        }
    }
#endif
    return dwDefaultValue;
}   // ReadRegistryDword()

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 );
}

DWORD
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;
                        

    if ( RegOpenKeyEx( HKEY_LOCAL_MACHINE,
                       W3_PARAMETERS_KEY,
                       0,
                       KEY_READ,
                       &hkeyParam ) == NO_ERROR )
    {
        fAllowSpecialCharsInShell = !!ReadRegistryDword( hkeyParam,
                                                "AllowSpecialCharsInShell",
                                                FALSE );

        fForwardServerEnvironmentBlock = !!ReadRegistryDword( 
                hkeyParam,
                "ForwardServerEnvironmentBlock",
                TRUE );

        fCreateProcessAsUser = !!ReadRegistryDword( hkeyParam,
                                                    "CreateProcessAsUser",
                                                    TRUE );

        fCreateProcessWithNewConsole = !!ReadRegistryDword( hkeyParam,
                "CreateProcessWithNewConsole",
                FALSE );

        dwTimeOut = ReadRegistryDword( hkeyParam,
                                       "ScriptTimeOut",
                                       SSI_CGI_DEF_TIMEOUT );
        if ( dwTimeOut >= ((DWORD)-1)/1000 )
        {
            dwTimeOut = (DWORD)-1;
        }
        else
        {
            dwTimeOut *= 1000;
        }

        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(CHAR))) == NULL )
        {
            return ERROR_NOT_ENOUGH_MEMORY;
        }

        memcpy( g_pszIisEnv, pvEnv, 
                cchIisEnv * sizeof(CHAR) );

        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;
    }

    fInitialized = TRUE;

    return NO_ERROR;
}


VOID 
TerminateCGI( 
    VOID 
    )
/*++

Routine Description:

    Terminate CGI


Arguments:

    None

Return Value:

    TRUE if successful, FALSE on error

--*/
{
    if (g_pszIisEnv != NULL )
    {
        LocalFree( g_pszIisEnv );
        g_pszIisEnv = NULL;
    }

    if ( g_pEnvEntries && g_pEnvEntries != CGIEnvTable )
    {
        delete [] g_pEnvEntries;
        g_pEnvEntries = NULL;
    }
    fInitialized = FALSE;
}

BOOL
ProcessCGI(
    SSI_REQUEST *       pRequest,
    const STR *         pstrPath,
    const STR *         pstrQueryString,
    const STR *         pstrWorkingDir,
    const STR *         pstrCmdLine,
    const STR *         pstrPathInfo
    )
/*++

Routine Description:

    Processes a CGI client request

Arguments:

    pRequest - SSI_REQUEST struct used to access ISAPI utilities
    pstrPath - Fully qualified path to executable (or NULL if the module
        is contained in pstrCmdLine)
    pstrQueryString - Parameter list for command (or NULL if the parms
        are specified in pstrCmdLine)
    strWorkingDir - Working directory for spawned process
        (generally the web root).  Can be NULL -> then CWD of command is used
    pstrCmdLine - Optional command line to use instead of the default
    pstrPathInfo - Path Info

Return Value:

    TRUE if successful, FALSE on error

--*/
{
    STARTUPINFO          startupinfo;
    PROCESS_INFORMATION  processinfo;
    BUFFER               buffEnv;
    BOOL                 fRet = FALSE;
    CGI_INFO           * pCGIInfo = NULL;
    DWORD                dwThreadId;
    HANDLE               hThread;
    STR                  strCmdLine;
    DWORD                dwFlags = DETACHED_PROCESS;
    DWORD                dwThreadCode;

    TCP_ASSERT( fInitialized );

    if ( !SetupChildEnv( pRequest,
                        &buffEnv,
                        pstrPathInfo,
                        pstrQueryString ) )
    {
        LPCTSTR apszParms[ 1 ];
        CHAR pszNumBuf[ SSI_MAX_NUMBER_STRING ];
        _ultoa( GetLastError(), pszNumBuf, 10 );
        apszParms[ 0 ] = pszNumBuf;
        
        pRequest->SSISendError( SSINCMSG_CANT_SETUP_CHILD_ENV,
                                apszParms );

        return FALSE;
    } 

    if ( pstrCmdLine == NULL )
    {
        if ( !strCmdLine.Resize( SSI_MAX_PATH + 1 ) ||
             !strCmdLine.Copy( "\"" )       ||
             !strCmdLine.Append( pstrPath ? pstrPath->QueryStr() :
                                            NULL )   ||
             !strCmdLine.Append( "\" " ))
        {
            return FALSE;
        }
        if ( !SetupCmdLine( &strCmdLine,
                            *pstrQueryString ))
        {
            return FALSE;
        }
        pstrCmdLine = &strCmdLine;
    }

    TCP_ASSERT( pstrCmdLine != NULL );

    //
    //  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 )
    {
        DWORD i;

        if ( IsCmdExe( pstrCmdLine->QueryStr()) )
        {
            //
            //  We'll either match one of the characters or the '\0'
            //

            i = strcspn( pstrCmdLine->QueryStr(), "&|(,;%<>" );

            if ( pstrCmdLine->QueryStr()[i] )
            {
                TCP_PRINT(( DBG_CONTEXT,
                            "[ProcessCGI] Refusing request for command shell due "
                            " to special characters\n" ));
                SetLastError( ERROR_INVALID_PARAMETER );
                return FALSE;
            }
        }
    }

    //
    //  Setup the pipes information
    //

    pCGIInfo = new CGI_INFO( pRequest );

    if ( !pCGIInfo )
    {
        SetLastError( ERROR_NOT_ENOUGH_MEMORY );
        return FALSE;
    }

    memset( &startupinfo, 0, sizeof(startupinfo) );
    startupinfo.cb = sizeof(startupinfo);

    //
    //  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 ) )
    {
        LPCTSTR apszParms[ 1 ];
        CHAR pszNumBuf[ SSI_MAX_NUMBER_STRING ];
        _ultoa( GetLastError(), pszNumBuf, 10 );
        apszParms[ 0 ] = pszNumBuf;
        
        pRequest->SSISendError( SSINCMSG_CANT_SETUP_CHILD_PIPES,
                                apszParms );
        goto Exit;
    }

    if ( fCreateProcessWithNewConsole )
    {
        dwFlags = CREATE_NEW_CONSOLE | CREATE_SEPARATE_WOW_VDM;
    }

    //
    //  Spawn the process and close the handles since we don't need them
    //

TryAgain:

    if ( !fCreateProcessAsUser )
    {
        fRet = CreateProcess( pstrPath ? pstrPath->QueryStr() : NULL,
                              pstrCmdLine->QueryStr(),
                              NULL,      // Process security
                              NULL,      // Thread security
                              TRUE,      // Inherit handles
                              dwFlags,
                              buffEnv.QueryPtr(),
                              pstrWorkingDir != NULL ?
                                    pstrWorkingDir->QueryStr() : NULL,
                              &startupinfo,
                              &processinfo );
    }
    else
    {
        HANDLE      hToken;

        TCP_REQUIRE( RevertToSelf() );

        hToken = pRequest->QueryPrimaryToken();

        if ( !ImpersonateLoggedOnUser( hToken ))
        {
            TCP_PRINT(( DBG_CONTEXT,
                        "[ProcessCGI] ImpersonateLoggedOnUser failed, error %lx\n",
                        GetLastError() ));
            goto Exit;
        }

        fRet = CreateProcessAsUser( hToken,
                                    (pstrPath ? pstrPath->QueryStr() : NULL),
                                    pstrCmdLine->QueryStr(),
                                    NULL,      // Process security
                                    NULL,      // Thread security
                                    TRUE,      // Inherit handles
                                    dwFlags,
                                    buffEnv.QueryPtr(),
                                    pstrWorkingDir != NULL ?
                                        pstrWorkingDir->QueryStr() : NULL,
                                    &startupinfo,
                                    &processinfo );
    }

    //
    //  If we get access denied this may be a 16 bit app, so try again w/o
    //  the process detached flag
    //

    if ( !fRet &&
        (GetLastError() == ERROR_ACCESS_DENIED ||
         GetLastError() == ERROR_INVALID_HANDLE ) &&
         (dwFlags & DETACHED_PROCESS) )
    {
        dwFlags &= ~DETACHED_PROCESS;
        goto TryAgain;
    }

    TCP_REQUIRE( CloseHandle( startupinfo.hStdOutput ) );
    TCP_REQUIRE( CloseHandle( startupinfo.hStdInput ) );

    if ( !fRet )
    {
        LPCTSTR apszParms[ 2 ];
        CHAR pszNumBuf[ SSI_MAX_NUMBER_STRING ];
        DWORD dwError = GetLastError();
        _ultoa( dwError, pszNumBuf, 10 );
        apszParms[ 0 ] = pstrCmdLine->QueryStr(),
        apszParms[ 1 ] = pszNumBuf;
        
        pRequest->SSISendError( SSINCMSG_CANT_CREATE_PROCESS,
                                apszParms );
        
        TCP_PRINT((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;
    }

    TCP_REQUIRE( CloseHandle( processinfo.hThread ) );
    TCP_ASSERT( startupinfo.hStdError == startupinfo.hStdOutput);

    //
    //  Save the process handle in case we need to terminate it later on
    //

    pCGIInfo->_hProcess = processinfo.hProcess;

    //
    //  Create a thread to handle IO with the child process
    //

    if ( !(hThread = CreateThread( NULL,
                                   0,
                                   (LPTHREAD_START_ROUTINE) CGIThread,
                                   pCGIInfo,
                                   0,
                                   &dwThreadId )))
    {
        goto Exit;
    }

    //
    //  Here is a crude method of finding runaway/hanging processes
    //

    DWORD dwWaitRes;
    while ( !pCGIInfo->_fShutdown )
    {
        dwWaitRes = WaitForSingleObject( pCGIInfo->_hResponseEvent,
                                         dwTimeOut );
        if ( dwWaitRes == WAIT_OBJECT_0 )
        {
            // got signal in time
            continue;
        }
        else if ( dwWaitRes == WAIT_TIMEOUT )
        {
            // process dead?  Terminating should cause thread to quit
            TerminateProcess( processinfo.hProcess,
                              SSI_KILLED_PROCESS );

            LPCTSTR apszParms[ 1 ];
            apszParms[ 0 ] = pstrCmdLine->QueryStr();

            pRequest->SSISendError( SSINCMSG_PROCESS_TIMEOUT,
                                    apszParms );
            
            break;
        }
    }

    WaitForSingleObject( hThread, INFINITE );

    if ( !GetExitCodeThread( hThread,
                             &dwThreadCode ) ||
         !dwThreadCode )
    {
        fRet = FALSE;
    }

    TCP_REQUIRE( CloseHandle( hThread ) );

Exit:
    delete pCGIInfo;

    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;

    return TRUE;

ErrorExit:
    if ( *phParentIn )
        TCP_REQUIRE( CloseHandle( *phParentIn ));

    if ( *phParentOut )
        TCP_REQUIRE( CloseHandle( *phParentOut ));

    return FALSE;

}

/*******************************************************************

    NAME:       SetupChildEnv

    SYNOPSIS:   Based on the passed pRequest, builds a CGI environment block

    ENTRY:      pRequest - HTTP request object
                pBuff - Buffer to receive environment block
                pstrPathInfo - PATH_INFO specified in #EXEC
                               (default is that of .STM file)
                pstrQueryString - QUERY_STRING specified in #EXEC
                                  (default is that of .STM file)

    RETURNS:    TRUE if successful, FALSE on failure

    HISTORY:
        Johnl       22-Sep-1994 Created

********************************************************************/

BOOL SetupChildEnv( SSI_REQUEST *   pRequest,
                    BUFFER *        pBuff,
                    const STR *     pstrPathInfo,
                    const STR *     pstrQueryString )
{
    CHAR * pch, *pchtmp;
    STR     strVal;
    UINT    cchCurrentPos = 0;      // Points to '\0' in buffer
    UINT    cchName, cchValue;
    UINT    cbNeeded;
    int     i = 0;

    if ( !pBuff->Resize( 1500 * sizeof(CHAR) ))
    {
        return FALSE;
    }

    //
    //  Build the environment block for CGI
    //

    while ( g_pEnvEntries[i].m_pszName )
    {
        //
        // 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(CHAR) ) )
            {
                return FALSE;
            }

            pch = (CHAR *) 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;
        }

        //
        // Can't use pRequest->GetVariable() for all variables
        // -> need to manually set some as specified in #EXEC directive
        //

        if ( pstrPathInfo && !strcmp( pch, "PATH_INFO" ) )
        {
            if ( !strVal.Copy( pstrPathInfo->QueryStr() ) )
            {
                return FALSE;
            }
        }
        else if ( pstrPathInfo && !strcmp( pch, "PATH_TRANSLATED" ) )
        {
            if ( !pRequest->LookupVirtualRoot( pstrPathInfo->QueryStr(),
                                               &strVal,
                                               0 ) )
            {
                return FALSE;
            }
        }
        else if ( pstrQueryString && !strcmp( pch, "QUERY_STRING" ) )
        {
            if ( !strVal.Copy( pstrQueryString->QueryStr() ) )
            {
                return FALSE;
            }
        }
        else
        {
            if ( !pRequest->GetVariable( pch,
                                    &strVal ) )
            {
                return FALSE;
            }
        }
        
        cchName = strlen( g_pEnvEntries[i].m_pszName );
        cchValue = strVal.QueryCCH();

        //
        //  We need space for the terminating '\0' and the '='
        //

        cbNeeded = ( cchName + cchValue + 1 + 1) * sizeof(CHAR);

        if ( !pBuff->Resize( cchCurrentPos * sizeof(CHAR) + 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 = (CHAR *) pBuff->QueryPtr();

        if ( *g_pEnvEntries[i].m_pszName )
        {
            if ( strVal.QueryStr()[0] )
            {
                memcpy( pch + cchCurrentPos,
                        g_pEnvEntries[i].m_pszName,
                        cchName * sizeof(CHAR));

                *(pch + cchCurrentPos + cchName) = '=';

                memcpy( pch + cchCurrentPos + cchName + 1,
                        strVal.QueryStr(),
                        (cchValue + 1) * sizeof(CHAR));

                cchCurrentPos += cchName + cchValue + 1 + 1;
            }
        }
        else
        {
            memcpy( pch + cchCurrentPos + cchName,
                    strVal.QueryStr(),
                    (cchValue + 1) * sizeof(CHAR));

            cchCurrentPos += cchName + cchValue;
        }

        i++;
    }

    //
    //  Add a '\0' terminator to the environment list
    //

    if ( !pBuff->Resize( (cchCurrentPos + 1) * sizeof(CHAR)))
    {
        return FALSE;
    }

    *((CHAR *) pBuff->QueryPtr() + cchCurrentPos) = '\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 )
{
    CHAR * pch;

    //
    //  If an unencoded "=" is found, don't use the command line
    //  (some weird CGI rule)
    //

    if ( strchr( strParams.QueryStr(),
                  '=' ))
    {
        return TRUE;
    }

    //
    //  Replace "+" with spaces and decode any hex escapes
    //

    if ( !pstrCmdLine->Append( strParams ) )
        return FALSE;

    while ( pch = strchr( pstrCmdLine->QueryStr(),
                           '+' ))
    {
        *pch = ' ';
        pch++;
    }

    return pstrCmdLine->Unescape();
}

/*******************************************************************

    NAME:       CGIThread

    SYNOPSIS:   Sends any gateway data to the scripts stdin and forwards
                the script's stdout to the client through ISAPI

    ENTRY:      Param - Pointer to CGI_INFO structure

    HISTORY:
        Johnl       22-Sep-1994 Created

********************************************************************/

DWORD CGIThread( PVOID Param )
{
    CGI_INFO     * pCGIInfo = (CGI_INFO *) Param;
    SSI_REQUEST *  pRequest = pCGIInfo->_pRequest;
    BYTE           buff[2048];
    DWORD          cbRead;
    DWORD          dwExitCode;
    DWORD          err;
    BOOL           fReadHeaders = FALSE;
    BOOL           fRet = TRUE;
    BOOL           fSuccess = TRUE;
    
    //
    //  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 );

        //
        // Let the calling thread know that the process
        // is still responding
        //

        SetEvent( pCGIInfo->_hResponseEvent );

        if ( !fRet )
        {
            err = GetLastError();
            if ( err == ERROR_BROKEN_PIPE )
            {
                break;
            }
            fSuccess = FALSE;
            TCP_PRINT((DBG_CONTEXT,
                       "[CGI_THREAD] ReadFile from child stdout failed, error %d, _hStdIn = %x\n",
                       GetLastError(),
                       pCGIInfo->_hStdIn));
            break;
        }

        //
        //  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 )
        {
            if ( !ProcessCGIInput( pCGIInfo,
                                   buff,
                                   cbRead,
                                   &fReadHeaders ) )
            {
                TCP_PRINT((DBG_CONTEXT,
                          "[CGIThread] ProcessCGIInput failed with error %d\n",
                           GetLastError()));
                fSuccess = FALSE;
                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 ( !pRequest->WriteToClient( buff,
                                      cbRead,
                                      &cbRead ) )
        {
            fSuccess = FALSE;
            break;
        }
    }

    //
    //  If we had to kill the process, log an error message
    //

    if ( GetExitCodeProcess( pCGIInfo->_hProcess,
                             &dwExitCode )  &&
         dwExitCode == SSI_KILLED_PROCESS )
    {
        TCP_PRINT((DBG_CONTEXT,
                  "[CGI_THREAD] - Spawned process hung\n" ));
        fSuccess = FALSE;
    }
    else
    {
        if ( !fReadHeaders )
        {
            DWORD cbSent;

            //
            // If we never read any headers, thats OK since were just
            // including the CGI output into HTML stream.
            //

            if( !pRequest->WriteToClient( pCGIInfo->_Buff.QueryPtr(),
                                          pCGIInfo->_cbData,
                                          &cbSent ) )
            {
                fSuccess = FALSE;
            }
        }
    }

    pCGIInfo->_fShutdown = TRUE;
    SetEvent( pCGIInfo->_hResponseEvent );

    return fSuccess;
}

/*******************************************************************

    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

    HISTORY:
        Johnl       22-Sep-1994 Created

********************************************************************/


BOOL ProcessCGIInput( CGI_INFO * pCGIInfo,
                      BYTE     * buff,
                      DWORD      cbRead,
                      BOOL     * pfReadHeaders )
{
    CHAR *          pchValue;
    CHAR *          pchField;
    BYTE *          pbData;
    DWORD           cbData;
    DWORD           cbSent;
    BOOL            fFoundStatus = FALSE;
    SSI_REQUEST *   pRequest = pCGIInfo->_pRequest;

    TCP_ASSERT( cbRead > 0 );

    if ( !pCGIInfo->_Buff.Resize( pCGIInfo->_cbData + cbRead,
                                  256 ))
    {
        return FALSE;
    }

    memcpy( (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 and look for
    //  Location: xxxx 
    //  URI: xxxx
    //
    //  In both cases, send a redirect message to HTML stream
    //  For all other headers, simply ignore them (i.e. dont send them)  
    //

    INET_PARSER Parser( (CHAR *) pCGIInfo->_Buff.QueryPtr() );
    while ( *(pchField = Parser.QueryToken()) )
    {
        Parser.SkipTo( ':' );
        Parser += 1;
        pchValue = Parser.QueryToken();

        if ( !::_strnicmp( "Location", pchField, 8 ) ||
             !::_strnicmp( "URI", pchField, 3 ))
        {
            //
            //  The CGI script is redirecting us to another URL.
            //  Just insert into HTML stream a message indicating this
            //  redirection
            //

            STR strURL( pchValue );
            STR strString;
            STR strResp;
            int iLen;

            if ( !strString.LoadString( SSINCMSG_CGI_REDIRECT_RESPONSE,
                                SSI_DLL_NAME ) )
            {
                return FALSE;
            }

            if ( !strResp.Resize( SSI_MAX_ERROR_MESSAGE + 1 ) )
            {
                return FALSE;
            }

            iLen = _snprintf( strResp.QueryStr(),
                              SSI_MAX_ERROR_MESSAGE + 1,
                              strString.QueryStr(),
                              strURL.QueryStr() );

            if ( !pRequest->WriteToClient( strResp.QueryStr(),
                                          iLen,
                                          (DWORD*) &iLen ) )
            {
                return FALSE;
            }
        }
        Parser.NextLine();
    }

    //
    //  If there was additional data in the buffer, send that out now
    //

    if ( cbData )
    {
        if ( !pRequest->WriteToClient( pbData,
                                       cbData,
                                       &cbSent ) )
        {
            return FALSE;
        }
    }
    return TRUE; 
}

/*******************************************************************

    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
    //

    //
    // 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 - (*ppbExtraData - (BYTE *) pchReq);
        *pfTerminated = TRUE;
        return TRUE;
    }

not_term:

    *pfTerminated = FALSE;

    //
    //  We didn't find the end so increase our buffer size
    //  in anticipation of more data
    //

    return pbuff->Resize( cbData + cbReallocSize );
}

BOOL
FastScanForTerminator(
    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.

--*/
{
    if ( cbData > 4 )
    {
        if ( !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  ) )
        {
            return TRUE;
        }
    }

    return FALSE;
}

/*******************************************************************

    NAME:       ::SkipWhite

    SYNOPSIS:   Skips white space starting at the passed point in the string
                and returns the next non-white space character.

    HISTORY:
        Johnl       23-Aug-1994 Created

********************************************************************/

CHAR * SkipWhite( CHAR * pch )
{
    while ( ISWHITEA( *pch ) )
    {
        pch++;
    }

    return pch;
}


BYTE *
ScanForTerminator(
    CHAR * 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;
}

BOOL
IsCmdExe(
    const CHAR * pchPath
    )
{
    while ( *pchPath )
    {
        if ( *pchPath == 'c' || *pchPath == 'C' )
        {
            if ( !_strnicmp( pchPath, "cmd.exe", sizeof("cmd.exe") - 1))
            {
                return TRUE;
            }
        }

        pchPath++;
    }

    return FALSE;
}