/*++

   Copyright    (c)    1999    Microsoft Corporation

   Module Name :
     urlinfo.cxx

   Abstract:
     Gets metadata for URL
 
   Author:
     Bilal Alam (balam)             8-Jan-2000

   Environment:
     Win32 - User Mode

   Project:
     ULW3.DLL
--*/

#include "precomp.hxx"
#include <stringau.hxx>

ALLOC_CACHE_HANDLER *   URL_CONTEXT::sm_pachUrlContexts;

//
// Utility to guard against ~ inconsistency
//

DWORD
CheckIfShortFileName(
    IN WCHAR *          pszPath,
    IN HANDLE           hImpersonation,
    OUT BOOL *          pfShort
);

W3_STATE_URLINFO::W3_STATE_URLINFO()
{
    _hr = URL_CONTEXT::Initialize();
}

W3_STATE_URLINFO::~W3_STATE_URLINFO()
{
    URL_CONTEXT::Terminate();
}

CONTEXT_STATUS
W3_STATE_URLINFO::OnCompletion(
    W3_MAIN_CONTEXT *       pMainContext,
    DWORD                   cbCompletion,
    DWORD                   dwCompletionStatus
)
/*++

Routine Description:

    Handle URLINFO completions.  CheckAccess() is called in DoWork() and this
    call is asynchronous.

Arguments:

    pMainContext - W3_MAIN_CONTEXT representing execution of state machine
    cbCompletion - Number of bytes in an async completion
    dwCompletionStatus - Error status of a completion
    
Return Value:

    CONTEXT_STATUS_CONTINUE - if we should continue in state machine
    else stop executing the machine and free up the current thread

--*/
{
    CONTEXT_STATUS              contextStatus;
    BOOL                        fAccessAllowed;
    
    contextStatus = pMainContext->CheckAccess( TRUE,   // this is a completion
                                               dwCompletionStatus,
                                               &fAccessAllowed );
                                 
    if ( contextStatus == CONTEXT_STATUS_PENDING )
    {
        return CONTEXT_STATUS_PENDING;
    }
    
    //
    // If access is not allowed, then just finish state machine (
    // response has already been sent)
    //
    
    if ( !fAccessAllowed )
    {
        pMainContext->SetFinishedResponse();
    }
    
    return CONTEXT_STATUS_CONTINUE;
}

CONTEXT_STATUS
W3_STATE_URLINFO::DoWork(
    W3_MAIN_CONTEXT *       pMainContext,
    DWORD                   cbCompletion,
    DWORD                   dwCompletionStatus
)
/*++

Routine Description:

    Handle retrieving the metadata for this request

Arguments:

    pMainContext - W3_MAIN_CONTEXT representing execution of state machine
    cbCompletion - Number of bytes in an async completion
    dwCompletionStatus - Error status of a completion
    
Return Value:

    CONTEXT_STATUS_CONTINUE - if we should continue in state machine
    else stop executing the machine and free up the current thread

--*/
{
    URL_CONTEXT *           pUrlContext = NULL;
    BOOL                    fFinished = FALSE;
    HRESULT                 hr = NO_ERROR;
    W3_METADATA *           pMetaData = NULL;
    CONTEXT_STATUS          contextStatus = CONTEXT_STATUS_CONTINUE;
    W3_REQUEST *            pHttpRequest = pMainContext->QueryRequest();
    W3_RESPONSE *           pResponse = pMainContext->QueryResponse();
    BOOL                    fAccessAllowed = FALSE;

    DBG_ASSERT( pHttpRequest != NULL );
    DBG_ASSERT( pResponse != NULL );

    //
    // Set the context state
    //
    
    hr = URL_CONTEXT::RetrieveUrlContext( pMainContext,
                                          pMainContext->QueryRequest(),
                                          &pUrlContext,
                                          &fFinished );
    if ( FAILED( hr ) )
    {
        goto Failure;
    } 
    
    DBG_ASSERT( fFinished || ( pUrlContext != NULL ) );
    pMainContext->SetUrlContext( pUrlContext );

    //
    // From now on, errors in this function should not cleanup the URL
    // context since it is owned by the main context
    //

    pUrlContext = NULL;

    //
    // If filter wants out, leave
    //

    if ( fFinished )
    {
        pMainContext->SetDone();
        return CONTEXT_STATUS_CONTINUE;
    }
    
    //
    // Check access now.  That means checking for IP/SSL/Certs.  We will 
    // avoid the authentication type check since the others (IP/SSL/Certs)
    // take priority.
    //
    
    contextStatus = pMainContext->CheckAccess( FALSE,     // not a completion
                                               NO_ERROR,
                                               &fAccessAllowed );
    if ( contextStatus == CONTEXT_STATUS_PENDING )
    {
        return CONTEXT_STATUS_PENDING;
    }
    
    //
    // If we don't have access, then the appropriate error response was
    // already sent.  Just finish the state machine
    //
    
    if ( !fAccessAllowed )
    {
        pMainContext->SetFinishedResponse();
    }   
    
    return CONTEXT_STATUS_CONTINUE;

Failure:

    if ( pUrlContext != NULL )
    {
        delete pUrlContext;
    }

    if ( !pMainContext->QueryResponseSent() )
    {
        if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
        {
            // For the non-8dot3 case
            pMainContext->QueryResponse()->SetStatus( HttpStatusNotFound );
        }
        else
        {
            pMainContext->QueryResponse()->SetStatus( HttpStatusServerError );
        }

        pMainContext->SetFinishedResponse();
        pMainContext->SetErrorStatus( hr );
    }

    return CONTEXT_STATUS_CONTINUE;
}

//static
HRESULT
URL_CONTEXT::RetrieveUrlContext(
    W3_CONTEXT *            pW3Context,
    W3_REQUEST *            pRequest,
    OUT URL_CONTEXT **      ppUrlContext,
    BOOL *                  pfFinished
)
/*++

Routine Description:

    For a given request, get a URL_CONTEXT which represents the 
    metadata and URI-specific info for that request

Arguments:

    pW3Context - W3_CONTEXT for the request
    pRequest - New request to lookup
    ppUrlContext - Set to point to new URL_CONTEXT
    pfFinished - Set to true if isapi filter said we're finished

Return Value:

    HRESULT

--*/
{
    STACK_STRU(         strUrl, MAX_PATH );
    W3_URL_INFO *       pUrlInfo = NULL;
    W3_METADATA *       pMetaData = NULL;
    URL_CONTEXT *       pUrlContext = NULL;
    HRESULT             hr = NO_ERROR;
    HANDLE              hToken = NULL;

    if ( pW3Context == NULL ||
         pRequest == NULL ||
         ppUrlContext == NULL ||
         pfFinished == NULL )
    {
        DBG_ASSERT( FALSE );
        return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER );
    }
    *ppUrlContext = NULL;

    hr = pRequest->GetUrl( &strUrl );
    if ( FAILED( hr ) )
    {
        goto Failure;
    }
    
    //
    // Lookup the URI info for this request
    //
    
    DBG_ASSERT( g_pW3Server->QueryUrlInfoCache() != NULL );
    
    hr = g_pW3Server->QueryUrlInfoCache()->GetUrlInfo( 
                                        pW3Context,
                                        strUrl,
                                        &pUrlInfo );
    if ( FAILED( hr ) )
    {
        goto Failure;
    }
    
    //
    // Now, create a URL_CONTEXT object which contains the W3_URL_INFO and
    // W3_METADATA pointers as well as state information for use on cleanup
    //
    
    DBG_ASSERT( pUrlInfo != NULL );

    pMetaData = (W3_METADATA*) pUrlInfo->QueryMetaData();
    DBG_ASSERT( pMetaData != NULL );

    pUrlContext = new URL_CONTEXT( pMetaData, pUrlInfo );
    if ( pUrlContext == NULL )
    {
        hr = HRESULT_FROM_WIN32( ERROR_NOT_ENOUGH_MEMORY );
        goto Failure;
    }
    
    //
    // Now notify URL_MAP filters
    //

    if ( pW3Context->IsNotificationNeeded( SF_NOTIFY_URL_MAP ) )
    {
        STACK_STRA(         straPhys, MAX_PATH + 1 );
        STACK_STRA(         straUrl, MAX_PATH + 1 );
        BOOL                fRet;
        HTTP_FILTER_URL_MAP filterMap;
        STACK_STRU(         strPhysicalPath, MAX_PATH );
        
        hr = straPhys.CopyW( pUrlInfo->QueryPhysicalPath()->QueryStr() );
        if ( FAILED( hr ) )
        {
            goto Failure;
        }
        
        hr = straUrl.CopyW( strUrl.QueryStr() );
        if ( FAILED( hr ) )
        {
            goto Failure;
        }

        filterMap.pszURL = straUrl.QueryStr();
        filterMap.pszPhysicalPath = straPhys.QueryStr();
        filterMap.cbPathBuff = MAX_PATH + 1;
       
        fRet = pW3Context->NotifyFilters( SF_NOTIFY_URL_MAP,
                                          &filterMap,
                                          pfFinished );

        //
        // If the filter is done, then we're done
        // 

        if ( *pfFinished )
        {
            hr = NO_ERROR;
            goto Failure;
        }
        
        //
        // If the physical path was changed, remember it here
        //
        
        hr = strPhysicalPath.CopyA( (CHAR*) filterMap.pszPhysicalPath );
        if ( FAILED( hr ) )
        {
            goto Failure;
        }
        
        if ( pUrlInfo->QueryPhysicalPath()->QueryCCH() != strPhysicalPath.QueryCCH() ||
             wcscmp( pUrlInfo->QueryPhysicalPath()->QueryStr(),
                     strPhysicalPath.QueryStr() ) != 0 )
        {
            hr = pUrlContext->SetPhysicalPath( strPhysicalPath );
            if ( FAILED( hr ) )
            {
                goto Failure;
            }
        }
    }
    
    //
    // We don't accept short filename since they can break metabase
    // equivalency
    //

    if ( wcschr( pUrlContext->QueryPhysicalPath()->QueryStr(),
                 L'~' ) )
    {
        BOOL fShort = FALSE;
        
        if ( pMetaData->QueryVrAccessToken() != NULL )
        {
            hToken = pMetaData->QueryVrAccessToken()->QueryImpersonationToken();
        }
        else
        {
            hToken = NULL;
        }
        
        
        DWORD dwError = CheckIfShortFileName( 
                                pUrlContext->QueryPhysicalPath()->QueryStr(),
                                hToken,
                                &fShort );
        if ( dwError != ERROR_SUCCESS )
        {
            hr = HRESULT_FROM_WIN32( dwError ); 
            goto Failure;
        }

        if ( fShort )
        {
            hr = HRESULT_FROM_WIN32( ERROR_FILE_NOT_FOUND );
            goto Failure;
        }
    }
    
    *ppUrlContext = pUrlContext;
    return S_OK;

Failure:
    if ( pUrlContext != NULL )
    {
        delete pUrlContext;
    }
    else
    {
        if ( pUrlInfo != NULL )
        {
            pUrlInfo->DereferenceCacheEntry();
        }
    }
   
    return hr; 
}

//static
HRESULT
W3_STATE_URLINFO::MapPath(
    W3_CONTEXT *            pW3Context,
    STRU &                  strUrl,
    STRU *                  pstrPhysicalPath,
    DWORD *                 pcchDirRoot,
    DWORD *                 pcchVRoot,
    DWORD *                 pdwMask
)
/*++

Routine Description:

    Send a URL/Physical-Path pair to a filter for processing

Arguments:

    pW3Context - W3_CONTEXT for the request
    strUrl - The URL to be mapped
    pstrPhysicalPath - Filled with the mapped path upon return.  Set with
                       metadata physical path on entry
    pcchDirRoot - Set to point to number of characters in found physical path
    pcchVRoot - Set to point to number of characters in found virtual path
    pdwMask - Set to point to the access perms mask of virtual path

Return Value:

    SUCCEEDED()/FAILED()

--*/
{
    HRESULT         hr = S_OK;
    W3_URL_INFO *   pUrlInfo = NULL;
    W3_METADATA *   pMetaData = NULL;

    DBG_ASSERT( pstrPhysicalPath );
    
    //
    // Get and keep the metadata and urlinfo for this path
    //
    
    DBG_ASSERT( g_pW3Server->QueryUrlInfoCache() != NULL );
    
    hr = g_pW3Server->QueryUrlInfoCache()->GetUrlInfo( 
                                        pW3Context,
                                        strUrl,
                                        &pUrlInfo );
    if ( FAILED( hr ) )
    {
        goto Exit;
    }
    
    DBG_ASSERT( pUrlInfo != NULL );

    //
    // Call the filters
    //
    
    hr = FilterMapPath( pW3Context,
                        pUrlInfo,
                        pstrPhysicalPath );
    if ( FAILED( hr ) )
    {
        goto Exit;
    }
                        
    pMetaData = pUrlInfo->QueryMetaData();
    DBG_ASSERT( pMetaData != NULL );

    //
    // Return the other goodies
    //
    
    if ( pcchDirRoot != NULL )
    {
        *pcchDirRoot = pMetaData->QueryVrPath()->QueryCCH();
    }
    
    if ( pcchVRoot != NULL )
    {
        if (strUrl.QueryCCH())
        {
            *pcchVRoot = pMetaData->QueryVrLen();
        }
        else
        {
            *pcchVRoot = 0;
        }
    }
    
    if ( pdwMask != NULL )
    {
        *pdwMask = pMetaData->QueryAccessPerms();
    }

Exit:

    if ( pUrlInfo != NULL )
    {
        pUrlInfo->DereferenceCacheEntry();
        pUrlInfo = NULL;
    }
    
    return hr;
}

// static
HRESULT
W3_STATE_URLINFO::FilterMapPath(
    W3_CONTEXT *            pW3Context,
    W3_URL_INFO *           pUrlInfo,
    STRU *                  pstrPhysicalPath
    )
/*++

Routine Description:

    Have URL_MAP filters do their thing

Arguments:

    pW3Context - Context
    pUrlInfo - Contains virtual/physical path
    pstrPhysicalPath - Filled with physical path

Return Value:

    SUCCEEDED()/FAILED()

--*/
{
    HRESULT         hr = S_OK;
    BOOL            fFinished = FALSE;
    W3_METADATA *   pMetaData = NULL;
    STACK_STRU(     strFilterPath, MAX_PATH );
    STRU *          pstrFinalPhysical = NULL;

    if ( pW3Context == NULL ||
         pUrlInfo == NULL ||
         pstrPhysicalPath == NULL )
    {
        DBG_ASSERT( FALSE );
        return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER );
    }

    pMetaData = pUrlInfo->QueryMetaData();
    DBG_ASSERT( pMetaData != NULL );

    //
    // We now have the metadata physical path.  Let filters change it here
    //

    if ( pW3Context->IsNotificationNeeded( SF_NOTIFY_URL_MAP ) )
    {
        STACK_STRA(         straPhys, MAX_PATH + 1 );
        STACK_STRA(         straUrl, MAX_PATH + 1 );
        BOOL                fRet;
        HTTP_FILTER_URL_MAP filterMap;
        
        hr = straPhys.CopyW( pUrlInfo->QueryUrlTranslated()->QueryStr() );
        if ( FAILED( hr ) )
        {
            goto Exit;
        }
        
        hr = straUrl.CopyW( pUrlInfo->QueryUrl() );
        if ( FAILED( hr ) )
        {
            goto Exit;
        }

        filterMap.pszURL = straUrl.QueryStr();
        filterMap.pszPhysicalPath = straPhys.QueryStr();
        filterMap.cbPathBuff = MAX_PATH + 1;
       
        fRet = pW3Context->NotifyFilters( SF_NOTIFY_URL_MAP,
                                          &filterMap,
                                          &fFinished );

        //
        // Ignore finished flag in this case since we really can't do much
        // to advance to finish (since an ISAPI is calling this)
        // 
        
        if ( !fRet )
        {
            hr = HRESULT_FROM_WIN32( GetLastError() );
            goto Exit;
        }

        //
        // Remember the mapped path
        //
        
        hr = strFilterPath.CopyA( (CHAR*) filterMap.pszPhysicalPath );
        if ( FAILED( hr ) )
        {
            goto Exit;
        }
        
        pstrFinalPhysical = &strFilterPath;
    }
    else
    {
        //
        // No filter is mapping, therefore just take the URL_INFO's physical
        // path
        //   
        
        pstrFinalPhysical = pUrlInfo->QueryUrlTranslated();
        
        DBG_ASSERT( pstrFinalPhysical != NULL );
    }

    //
    // We don't accept short filename since they can break metabase
    // equivalency
    //

    if ( wcschr( pstrFinalPhysical->QueryStr(),
                 L'~' ) )
    {
        BOOL fShort = FALSE;
        DWORD dwError = CheckIfShortFileName( 
                                pstrFinalPhysical->QueryStr(),
                                pW3Context->QueryImpersonationToken(),
                                &fShort );
        if ( dwError != ERROR_SUCCESS )
        {
            hr = HRESULT_FROM_WIN32( dwError ); 
            goto Exit;
        }

        if ( fShort )
        {
            hr = HRESULT_FROM_WIN32( ERROR_FILE_NOT_FOUND );
            goto Exit;
        }
    }
    
    //
    // Copy the physical path is requested
    //
    
    hr = pstrPhysicalPath->Copy( *pstrFinalPhysical );
    if ( FAILED( hr ) )
    {
        goto Exit;
    }

Exit:

    return hr;
}

DWORD
CheckIfShortFileName(
    IN WCHAR *          pszPath,
    IN HANDLE           hImpersonation,
    OUT BOOL *          pfShort
)
/*++
    Description:

        This function takes a suspected NT/Win95 short filename and checks if there's
        an equivalent long filename.  For example, c:\foobar\ABCDEF~1.ABC is the same
        as c:\foobar\abcdefghijklmnop.abc.

        NOTE: This function should be called unimpersonated - the FindFirstFile() must
        be called in the system context since most systems have traverse checking turned
        off - except for the UNC case where we must be impersonated to get network access.

    Arguments:

        pszPath - Path to check
        hImpersonation - Impersonation handle if this is a UNC path - can be NULL if not UNC
        pfShort - Set to TRUE if an equivalent long filename is found

    Returns:

        Win32 error on failure
--*/
{
    DWORD              err = NO_ERROR;
    WIN32_FIND_DATA    FindData;
    WCHAR *            psz;
    BOOL               fUNC;

    psz = wcschr( pszPath, L'~' );
    *pfShort = FALSE;
    fUNC = (*pszPath == L'\\');

    //
    //  Loop for multiple tildas - watch for a # after the tilda
    //

    while ( psz++ )
    {
        if ( *psz >= L'0' && *psz <= L'9' )
        {
            WCHAR   achTmp[MAX_PATH];
            WCHAR * pchEndSeg;
            WCHAR * pchBeginSeg;
            HANDLE  hFind;

            //
            //  Isolate the path up to the segment with the
            //  '~' and do the FindFirst with that path
            //
            
            pchEndSeg = wcschr( psz, L'\\' );
            if ( !pchEndSeg )
            {
                pchEndSeg = psz + wcslen( psz );
            }

            //
            //  If the string is beyond MAX_PATH then we allow it through
            //

            if ( ((INT) (pchEndSeg - pszPath)) >= MAX_PATH )
            {
                return NO_ERROR;
            }

            memcpy( achTmp, 
                    pszPath, 
                    (INT) (pchEndSeg - pszPath) * sizeof( WCHAR ) );
            achTmp[pchEndSeg - pszPath] = L'\0';

            if ( fUNC && hImpersonation )
            {
                if ( !SetThreadToken( NULL, hImpersonation ))
                {
                    return GetLastError();
                }
            }

            hFind = FindFirstFileW( achTmp, &FindData );

            if ( fUNC && hImpersonation )
            {
                RevertToSelf();
            }

            if ( hFind == INVALID_HANDLE_VALUE )
            {
                err = GetLastError();

                DBGPRINTF(( DBG_CONTEXT,
                            "FindFirst failed!! - \"%s\", error %d\n",
                            achTmp,
                            GetLastError() ));

                //
                //  If the FindFirstFile() fails to find the file then return
                //  success - the path doesn't appear to be a valid path which
                //  is ok.
                //

                if ( err == ERROR_FILE_NOT_FOUND ||
                     err == ERROR_PATH_NOT_FOUND )
                {
                    return NO_ERROR;
                }

                return err;
            }

            DBG_REQUIRE( FindClose( hFind ));

            //
            //  Isolate the last segment of the string which should be
            //  the potential short name equivalency
            //

            pchBeginSeg = wcsrchr( achTmp, L'\\' );
            DBG_ASSERT( pchBeginSeg );
            pchBeginSeg++;

            //
            //  If the last segment doesn't match the long name then this is
            //  the short name version of the path
            //
            
            if ( _wcsicmp( FindData.cFileName, pchBeginSeg ))
            {
                *pfShort = TRUE;
                return NO_ERROR;
            }
        }

        psz = wcschr( psz, L'~' );
    }

    return err;
}

HRESULT
URL_CONTEXT::OpenFile(
    FILE_CACHE_USER *       pFileUser,
    W3_FILE_INFO **         ppOpenFile
)
/*++

Routine Description:

    Open the physical path for this request.  If a map path filter did some
    redirecting, we will use that path.  Otherwise we will just use the 
    path determined by metadata and cached in the W3_URL_INFO 

Arguments:

    pFileUser - User to open file as
    ppOpenFile - Set to file cache entry on success

Return Value:

    HRESULT

--*/
{
    HRESULT             hr;
    BOOL                fDoCache;
    
    DBG_ASSERT( QueryMetaData() != NULL );
    
    fDoCache = !QueryMetaData()->QueryNoCache();
    
    //
    // If an ISAPI filter changed the physical path, then we need to go 
    // directly to the file cache.  Otherwise, we can go thru the 
    // W3_URL_INFO which may already have the cached file associated
    //
    
    if ( _strPhysicalPath.IsEmpty() )
    {
        //
        // No filter.  Fast path :-)
        //
        
        DBG_ASSERT( _pUrlInfo != NULL );
        
        hr = _pUrlInfo->GetFileInfo( pFileUser,
                                     fDoCache,
                                     ppOpenFile );
    }
    else
    {
        //
        // Filter case.  Must lookup in file cache :-(
        //
        
        DBG_ASSERT( g_pW3Server->QueryFileCache() != NULL );
        
        hr = g_pW3Server->QueryFileCache()->GetFileInfo( 
                                        _strPhysicalPath,
                                        QueryMetaData()->QueryDirmonConfig(),
                                        pFileUser,
                                        fDoCache,
                                        ppOpenFile );
    }
    
    return hr;
}

//static
HRESULT
URL_CONTEXT::Initialize(
    VOID
)
/*++

Routine Description:

    Initialize URL_CONTEXT lookaside

Arguments:

    None
    
Return Value:

    HRESULT

--*/
{
    ALLOC_CACHE_CONFIGURATION       acConfig;
    HRESULT                         hr = NO_ERROR;

    //
    // Setup allocation lookaside
    //
    
    acConfig.nConcurrency = 1;
    acConfig.nThreshold = 100;
    acConfig.cbSize = sizeof( URL_CONTEXT );

    DBG_ASSERT( sm_pachUrlContexts == NULL );
    
    sm_pachUrlContexts = new ALLOC_CACHE_HANDLER( "URL_CONTEXT",  
                                                  &acConfig );

    if ( sm_pachUrlContexts == NULL )
    {
        return HRESULT_FROM_WIN32( ERROR_NOT_ENOUGH_MEMORY );
    }
    
    return NO_ERROR;
}

//static
VOID
URL_CONTEXT::Terminate(
    VOID
)
/*++

Routine Description:

    Clean up URL_CONTEXT lookaside

Arguments:

    None
    
Return Value:

    HRESULT

--*/
{
    if ( sm_pachUrlContexts != NULL )
    {
        delete sm_pachUrlContexts;
        sm_pachUrlContexts = NULL;
    }
}