Leaked source code of windows server 2003
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.
 
 
 
 
 
 

1191 lines
29 KiB

/*++
Copyright (c) 1999 Microsoft Corporation
Module Name :
urlinfo.cxx
Abstract:
Implementation of URL cache
Author:
Bilal Alam (balam) 8-Jan-2000
Environment:
Win32 - User Mode
Project:
ULW3.DLL
--*/
#include "precomp.hxx"
//
// This is the maximum size for a script map extension
//
#define MAX_EXT_LEN 128
ALLOC_CACHE_HANDLER * W3_URL_INFO::sm_pachW3UrlInfo;
DWORD W3_URL_INFO::sm_cMaxDots;
HRESULT
W3_URL_INFO_KEY::CreateCacheKey(
WCHAR * pszKey,
DWORD cchKey,
DWORD cchSitePrefix,
BOOL fCopy
)
/*++
Description:
Setup a URI cache key
Arguments:
pszKey - URL of cache key
cchKey - size of URL
cchSitePrefix - Size of site prefix ("LM/W3SVC/<n>")
fCopy - Set to TRUE if we should copy the URL, else we just keep a ref
Return:
HRESULT
--*/
{
HRESULT hr;
_cchSitePrefix = cchSitePrefix;
if ( fCopy )
{
hr = _strKey.Copy( pszKey );
if ( FAILED( hr ) )
{
return hr;
}
_pszKey = _strKey.QueryStr();
_cchKey = _strKey.QueryCCH();
}
else
{
_pszKey = pszKey;
_cchKey = cchKey;
}
return NO_ERROR;
}
//static
HRESULT
W3_URL_INFO::Initialize(
VOID
)
/*++
Description:
URI entry lookaside initialization
Arguments:
None
Return:
HRESULT
--*/
{
ALLOC_CACHE_CONFIGURATION acConfig;
HRESULT hr;
DWORD dwError;
HKEY hKey = NULL;
DWORD cbData;
DWORD dwType;
DWORD dwValue;
//
// Default max dots is 100
//
sm_cMaxDots = 100;
//
// Look for a registry override to the max dot value
// (one of the more useful configurable options in IIS)
//
dwError = RegOpenKeyEx( HKEY_LOCAL_MACHINE,
L"System\\CurrentControlSet\\Services\\inetinfo\\Parameters",
0,
KEY_READ,
&hKey );
if ( dwError == ERROR_SUCCESS )
{
DBG_ASSERT( hKey != NULL );
//
// Should we be file caching at all?
//
cbData = sizeof( DWORD );
dwError = RegQueryValueEx( hKey,
L"MaxDepthDots",
NULL,
&dwType,
(LPBYTE) &dwValue,
&cbData );
if ( dwError == ERROR_SUCCESS && dwType == REG_DWORD )
{
sm_cMaxDots = dwValue;
}
RegCloseKey( hKey );
}
//
// Initialize allocation lookaside
//
acConfig.nConcurrency = 1;
acConfig.nThreshold = 100;
acConfig.cbSize = sizeof( W3_URL_INFO );
DBG_ASSERT( sm_pachW3UrlInfo == NULL );
sm_pachW3UrlInfo = new ALLOC_CACHE_HANDLER( "W3_URL_INFO",
&acConfig );
if ( sm_pachW3UrlInfo == NULL )
{
hr = HRESULT_FROM_WIN32( ERROR_NOT_ENOUGH_MEMORY );
DBGPRINTF(( DBG_CONTEXT,
"Error initializing sm_pachW3UrlInfo. hr = 0x%x\n",
hr ));
return hr;
}
return NO_ERROR;
}
//static
VOID
W3_URL_INFO::Terminate(
VOID
)
/*++
Description:
URI cache cleanup
Arguments:
None
Return:
None
--*/
{
if ( sm_pachW3UrlInfo != NULL )
{
delete sm_pachW3UrlInfo;
sm_pachW3UrlInfo = NULL;
}
}
HRESULT
W3_URL_INFO::Create(
STRU & strUrl,
STRU & strMetadataPath
)
/*++
Description:
Initialize a W3_URL_INFO
Arguments:
strUrl - Url key for this entry
strMetadataPath - Full metadata path used for key
Return:
HRESULT
--*/
{
HRESULT hr;
hr = _cacheKey.CreateCacheKey( strMetadataPath.QueryStr(),
strMetadataPath.QueryCCH(),
strMetadataPath.QueryCCH() - strUrl.QueryCCH(),
TRUE );
if ( FAILED( hr ) )
{
return hr;
}
//
// Process the URL for execution info (and splitting path_info/url)
//
hr = ProcessUrl( strUrl );
if( FAILED( hr ) )
{
return hr;
}
//
// Now create physical path
//
hr = _pMetaData->BuildPhysicalPath( _strProcessedUrl,
&_strPhysicalPath );
if ( FAILED( hr ) )
{
return hr;
}
//
// Now create translation of the whole URL. Note: this is not always
// PathTranslated, so we do not name it so
//
hr = _pMetaData->BuildPhysicalPath( strUrl,
&_strUrlTranslated );
if ( FAILED( hr ) )
{
return hr;
}
return NO_ERROR;
}
HRESULT
W3_URL_INFO::GetPathTranslated(
W3_CONTEXT * pW3Context,
BOOL fUsePathInfo,
STRU * pstrPathTranslated
)
/*++
Routine Description:
Given the PATH_INFO of this cached entry, determine PATH_TRANSLATED.
This involves:
1) Mapping PATH_INFO to a physical path
2) Calling filters if necessary for this path
We cache the output of step 1)
Arguments:
pW3Context - W3Context
pstrPathTranslated - Filled with physical path on success
Return Value:
HRESULT
--*/
{
HRESULT hr = S_OK;
W3_URL_INFO * pUrlInfo = NULL;
BOOL fReadLocked = TRUE;
if ( pW3Context == NULL ||
pstrPathTranslated == NULL )
{
DBG_ASSERT( FALSE );
return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER );
}
//
// Check for cached path translated entry. If its there and flushed,
// then release it (and try again)
//
if ( _pUrlInfoPathTranslated != NULL )
{
_PathTranslatedLock.ReadLock();
if ( _pUrlInfoPathTranslated != NULL )
{
if ( _pUrlInfoPathTranslated->QueryIsFlushed() )
{
_PathTranslatedLock.ConvertSharedToExclusive();
//
// Remember to write unlock when we're done
//
fReadLocked = FALSE;
//
// Due to how lock works, someone may have freed it already
//
if ( _pUrlInfoPathTranslated != NULL )
{
_pUrlInfoPathTranslated->DereferenceCacheEntry();
_pUrlInfoPathTranslated = NULL;
}
}
else
{
_pUrlInfoPathTranslated->ReferenceCacheEntry();
pUrlInfo = _pUrlInfoPathTranslated;
}
}
if ( fReadLocked )
{
_PathTranslatedLock.ReadUnlock();
}
else
{
_PathTranslatedLock.WriteUnlock();
}
}
//
// Use the cached translated entry if there
//
if ( pUrlInfo == NULL )
{
//
// Get and keep the metadata and urlinfo for this path
//
DBG_ASSERT( g_pW3Server->QueryUrlInfoCache() != NULL );
if ( fUsePathInfo )
{
hr = g_pW3Server->QueryUrlInfoCache()->GetUrlInfo( pW3Context,
_strPathInfo,
&pUrlInfo );
}
else
{
pUrlInfo = this;
pUrlInfo->ReferenceCacheEntry();
}
if ( FAILED( hr ) )
{
return hr;
}
DBG_ASSERT( pUrlInfo != NULL );
//
// Store away the URL info, provided it is an empty string.
//
// Basically, we're trying to optimize the case of "/foobar.dll".
// In this case, path info is empty, and we want to avoid doing the
// extra URL lookup for empty string.
//
if ( pUrlInfo->QueryCached() &&
pUrlInfo->QueryUrl()[0] == L'\0' )
{
_PathTranslatedLock.WriteLock();
if ( _pUrlInfoPathTranslated == NULL )
{
_pUrlInfoPathTranslated = pUrlInfo;
}
else
{
pUrlInfo->DereferenceCacheEntry();
pUrlInfo = _pUrlInfoPathTranslated;
}
pUrlInfo->ReferenceCacheEntry();
_PathTranslatedLock.WriteUnlock();
}
}
DBG_ASSERT( pUrlInfo != NULL );
//
// Now call into the filter
//
hr = W3_STATE_URLINFO::FilterMapPath( pW3Context,
pUrlInfo,
pstrPathTranslated );
pUrlInfo->DereferenceCacheEntry();
return hr;
}
HRESULT
W3_URL_INFO::GetFileInfo(
CACHE_USER * pOpeningUser,
BOOL fDoCache,
W3_FILE_INFO ** ppFileInfo,
FILE_CACHE_ASYNC_CONTEXT * pAsyncContext,
BOOL * pfHandledSync,
BOOL fAllowNoBuffering,
BOOL fCheckForExistenceOnly
)
/*++
Routine Description:
Get file info associated with this cache entry. If it doesn't exist,
then go to file cache directly to open the file
Arguments:
pOpeningUser - Opening user
fDoCache - Should we cache the file
ppFileInfo - Set to file cache entry on success
pAsyncContext - In case an async read is desired, context with callback info rmation
pfHandledSync - Did the open complete synchronously
fAllowNoBuffering - If opening the file, can we open with no-OS-buffering
fCheckFOrExistenceOnly - Check for existence only
Return Value:
HRESULT
--*/
{
W3_FILE_INFO * pFileInfo = NULL;
BOOL fCleanup = FALSE;
HRESULT hr;
if ( ppFileInfo == NULL ||
pOpeningUser == NULL )
{
DBG_ASSERT( FALSE );
return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER );
}
*ppFileInfo = NULL;
//
// Do we already have a file entry associated? If so use it, if it was
// not already flushed
//
// BIG NOTE: We will also cache "existence-only" cache entries since
// that makes our "Check-Path-Existence" option work fast.
//
// What that means is if we're not being asked for an existence-check,
// then we need to not use an existence-only cache entry
//
LockCacheEntry();
if ( _pFileInfo != NULL )
{
if ( _pFileInfo->Checkout(pOpeningUser) )
{
pFileInfo = _pFileInfo;
}
else
{
_pFileInfo->DereferenceCacheEntry();
_pFileInfo = NULL;
}
}
UnlockCacheEntry();
//
// Is this an existence-only entry? Is it usable
//
if ( pFileInfo != NULL &&
pFileInfo->QueryFileHandle() == INVALID_HANDLE_VALUE &&
pFileInfo->QueryFileBuffer() == NULL &&
!fCheckForExistenceOnly )
{
//
// Doh. This entry isn't very useful for us. Clear it out
//
LockCacheEntry();
if ( _pFileInfo != NULL )
{
_pFileInfo = NULL;
}
UnlockCacheEntry();
pFileInfo->DereferenceCacheEntry();
pFileInfo = NULL;
//
// Fall thru and act as if we never had a cached file to begin with
//
}
//
// If we got a file entry, we're done, assuming access check is ok
//
if ( pFileInfo != NULL )
{
hr = pFileInfo->DoAccessCheck( pOpeningUser );
if ( SUCCEEDED( hr ) )
{
*ppFileInfo = pFileInfo;
if (pfHandledSync != NULL)
{
*pfHandledSync = TRUE;
}
return NO_ERROR;
}
else
{
pFileInfo->DereferenceCacheEntry();
return hr;
}
}
//
// We'll have to go to file cache directly
//
DBG_ASSERT( g_pW3Server->QueryFileCache() != NULL );
hr = g_pW3Server->QueryFileCache()->GetFileInfo(
_strPhysicalPath,
_pMetaData->QueryDirmonConfig(),
pOpeningUser,
fDoCache,
&pFileInfo,
pAsyncContext,
pfHandledSync,
fAllowNoBuffering,
fCheckForExistenceOnly );
if ( FAILED( hr ) )
{
return hr;
}
//
// Now try to stuff the file descriptor into this object (bearing in mind
// that another thread may try to do the same thing for this W3_URL_INFO)
//
if ( ((pfHandledSync == NULL) || *pfHandledSync) &&
pFileInfo->QueryCached() )
{
LockCacheEntry();
if ( _pFileInfo == NULL )
{
pFileInfo->ReferenceCacheEntry();
_pFileInfo = pFileInfo;
}
UnlockCacheEntry();
}
//
// It is OK if this thread was not able to associate the file entry.
//
*ppFileInfo = pFileInfo;
return NO_ERROR;
}
HRESULT
W3_URL_INFO::ProcessUrl(
STRU & strUrl
)
/*++
Routine Description:
Process the URL and assocate execution information with
the W3_URL_INFO.
Called before adding a new URL info to the cache.
Look through the url for script-mapped or executable extensions
and update the W3_URL_INFO with the actual URL to execute,
path-info, gateway type, etc.
Arguments:
strUrl - Original requested URL
Return Value:
SUCCEEDED()/FAILED()
CODEWORK
1. Handle wildcard mappings
--*/
{
HRESULT hr = NOERROR;
DWORD cDots = 0;
DBG_ASSERT( _pMetaData );
STACK_STRU( strExtension, MAX_EXT_LEN );
//
// Reference the URL_INFO's data
//
STRU * pstrProcessedUrl = &_strProcessedUrl;
STRU * pstrPathInfo = &_strPathInfo;
//
// Iterate over pstrProcessedUrl. These always point at the
// pstrProcessedUrl string.
//
WCHAR * pszExtensionIter = NULL;
WCHAR * pszPathInfoIter = NULL;
//
// Make a working copy of the URL, this will be modified if an
// exectuable extension is found before the terminal node in the
// path.
//
hr = pstrProcessedUrl->Copy( strUrl );
if( FAILED(hr) )
{
goto failure;
}
//
// Search the URL for an extension that matches something
// we know how to execute.
//
pszExtensionIter = pstrProcessedUrl->QueryStr();
while( pszExtensionIter = wcschr( pszExtensionIter, L'.' ) )
{
//
// Maintain a count of the dots we encounter, any more than
// sm_cMaxDots and we fail the request
//
cDots++;
if ( cDots > sm_cMaxDots )
{
break;
}
//
// Save the extension string
//
hr = strExtension.Copy( pszExtensionIter );
if( FAILED(hr) )
{
goto failure;
}
//
// Find the end of the string or the beginning of the path info
//
pszPathInfoIter = wcschr( pszExtensionIter, L'/' );
if( pszPathInfoIter != NULL )
{
DBG_REQUIRE(
strExtension.SetLen(
DIFF(pszPathInfoIter - pszExtensionIter)
)
);
}
//
// Lowercase the extension string to allow case-insensitive
// comparisons.
//
_wcslwr( strExtension.QueryStr() );
//
// Try to find a matching script map entry
//
META_SCRIPT_MAP * pScriptMap;
META_SCRIPT_MAP_ENTRY * pScriptMapEntry;
pScriptMap = _pMetaData->QueryScriptMap();
DBG_ASSERT( pScriptMap );
if( pScriptMap->FindEntry( strExtension, &pScriptMapEntry ) )
{
DBG_ASSERT( pScriptMapEntry );
_pScriptMapEntry = pScriptMapEntry;
if( pszPathInfoIter )
{
hr = pstrPathInfo->Copy( pszPathInfoIter );
if( FAILED(hr) )
{
goto failure;
}
//
// Make sure that we truncate the URL so that we don't end
// up downloading a source file by mistake.
//
DBG_REQUIRE(
pstrProcessedUrl->SetLen(
DIFF(pszPathInfoIter - pstrProcessedUrl->QueryStr())
)
);
}
else
{
pstrPathInfo->Reset();
}
//
// Since we found the entry, we are done
//
break;
}
//
// No matching script map, so check if this is a wellknown
// Gateway type.
//
DBG_ASSERT( pScriptMapEntry == NULL );
DBG_ASSERT( _pScriptMapEntry == NULL );
//
// Avoid all the string comps if we will never match.
//
if( strExtension.QueryCCH() == 4 )
{
GATEWAY_TYPE Gateway;
Gateway = GATEWAY_UNKNOWN;
//
// Test extension against known gateway extensions.
//
// Does it make sense to allow the known extensions to be
// configured?
//
if( wcscmp( L".dll", strExtension.QueryStr() ) == 0 ||
wcscmp( L".isa", strExtension.QueryStr() ) == 0
)
{
Gateway = GATEWAY_ISAPI;
}
else if( !wcscmp( L".exe", strExtension.QueryStr() ) ||
!wcscmp( L".cgi", strExtension.QueryStr() ) ||
!wcscmp( L".com", strExtension.QueryStr() ) )
{
Gateway = GATEWAY_CGI;
}
else if( wcscmp( L".map", strExtension.QueryStr() ) == 0 )
{
Gateway = GATEWAY_MAP;
}
//
// OK. Before we continue, if the request was a GATEWAY_ISAPI
// or GATEWAY_CGI and we do NOT have EXECUTE permissions, then
// this really isn't a ISA/CGI after all
//
if ( Gateway == GATEWAY_CGI ||
Gateway == GATEWAY_ISAPI )
{
if ( !( _pMetaData->QueryAccessPerms() & VROOT_MASK_EXECUTE ) )
{
Gateway = GATEWAY_UNKNOWN;
}
}
//
// The gateway is specified in the URL and we recognize it.
//
if( Gateway != GATEWAY_UNKNOWN )
{
_Gateway = Gateway;
//
// Save everthing after the matching extension as
// path-info and truncate the URL so that it doesn't
// include the path info
//
if( pszPathInfoIter )
{
hr = pstrPathInfo->Copy( pszPathInfoIter );
if( FAILED(hr) )
{
goto failure;
}
DBG_REQUIRE(
pstrProcessedUrl->SetLen(
DIFF(pszPathInfoIter - pstrProcessedUrl->QueryStr())
)
);
}
else
{
pstrPathInfo->Reset();
}
//
// We have a match so exit the loop
//
break;
}
}
//
// We do not have a matching entry, so continue to look for an
// executable extension.
//
pszExtensionIter++;
} // while (pszExtensionIter)
//
// Now associate the ContentType for this entry
//
if (_pScriptMapEntry == NULL &&
_Gateway != GATEWAY_ISAPI &&
_Gateway != GATEWAY_CGI)
{
if (FAILED(hr = SelectMimeMappingForFileExt(pstrProcessedUrl->QueryStr(),
_pMetaData->QueryMimeMap(),
&_strContentType,
&_fDefaultMimeMap)))
{
goto failure;
}
}
return S_OK;
failure:
DBG_ASSERT( FAILED(hr) );
return FAILED(hr) ? hr : E_FAIL;
}
HRESULT
W3_URL_INFO_CACHE::Initialize(
VOID
)
/*++
Description:
Initialize URI cache
Arguments:
None
Return:
HRESULT
--*/
{
HRESULT hr;
DWORD dwData;
DWORD dwType;
DWORD cbData = sizeof( DWORD );
DWORD csecTTL = DEFAULT_W3_URL_INFO_CACHE_TTL;
HKEY hKey;
//
// What is the TTL for the URI cache
//
if ( RegOpenKeyEx( HKEY_LOCAL_MACHINE,
L"System\\CurrentControlSet\\Services\\inetinfo\\Parameters",
0,
KEY_READ,
&hKey ) == ERROR_SUCCESS )
{
DBG_ASSERT( hKey != NULL );
if ( RegQueryValueEx( hKey,
L"ObjectCacheTTL",
NULL,
&dwType,
(LPBYTE) &dwData,
&cbData ) == ERROR_SUCCESS &&
dwType == REG_DWORD )
{
csecTTL = dwData;
}
RegCloseKey( hKey );
}
//
// We'll use TTL for scavenge period, and expect two inactive periods to
// flush
//
hr = SetCacheConfiguration( csecTTL * 1000,
csecTTL * 1000,
CACHE_INVALIDATION_METADATA,
NULL );
if ( FAILED( hr ) )
{
return hr;
}
return W3_URL_INFO::Initialize();
}
HRESULT
W3_URL_INFO_CACHE::GetUrlInfo(
W3_CONTEXT * pW3Context,
STRU & strUrl,
W3_URL_INFO ** ppUrlInfo
)
/*++
Routine Description:
Retrieve a W3_URL_INFO, creating it if necessary
Arguments:
pW3Context - W3 context
strUrl - Url to lookup
ppUrlInfo - Filled with cache entry if successful
Return Value:
HRESULT
--*/
{
W3_URL_INFO_KEY uriKey;
W3_URL_INFO * pUrlInfo;
STACK_STRU( strKey, MAX_PATH );
STRU * pstrMBRoot;
HRESULT hr;
if ( pW3Context == NULL ||
ppUrlInfo == NULL )
{
DBG_ASSERT( FALSE );
return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER );
}
*ppUrlInfo = NULL;
//
// The key is the full metadata path. Start with the site prefix minus
// the trailing '/'
//
pstrMBRoot = pW3Context->QuerySite()->QueryMBRoot();
DBG_ASSERT( pstrMBRoot != NULL );
hr = strKey.Copy( pstrMBRoot->QueryStr(),
pstrMBRoot->QueryCCH() - 1 );
if ( FAILED( hr ) )
{
return hr;
}
//
// Now add the URL (note that some applications depend on
// case-sensitivity in the URL.)
//
hr = strKey.Append( strUrl );
if ( FAILED( hr ) )
{
return hr;
}
//
// Setup a key to lookup
//
hr = uriKey.CreateCacheKey( strKey.QueryStr(),
strKey.QueryCCH(),
pW3Context->QuerySite()->QueryMBRoot()->QueryCCH(),
FALSE );
if ( FAILED( hr ) )
{
return hr;
}
//
// Look it up
//
hr = FindCacheEntry( &uriKey,
(CACHE_ENTRY**) &pUrlInfo );
if ( SUCCEEDED( hr ) )
{
DBG_ASSERT( pUrlInfo != NULL );
*ppUrlInfo = pUrlInfo;
return NO_ERROR;
}
//
// We need to create a URI cache entry
//
hr = CreateNewUrlInfo( pW3Context,
strUrl,
strKey,
&pUrlInfo );
if ( FAILED( hr ) )
{
return hr;
}
DBG_ASSERT( pUrlInfo != NULL );
//
// Add to the cache
//
AddCacheEntry( pUrlInfo );
*ppUrlInfo = pUrlInfo;
return NO_ERROR;
}
HRESULT
W3_URL_INFO_CACHE::CreateNewUrlInfo(
W3_CONTEXT * pW3Context,
STRU & strUrl,
STRU & strMetadataPath,
W3_URL_INFO ** ppUrlInfo
)
/*++
Routine Description:
Create a new URI cache entry
Arguments:
pW3Context - Main context
strUrl - Url
strMetadataPath - Full metadata path used as key
ppUrlInfo - Set to URI cache entry
Return Value:
HRESULT
--*/
{
W3_METADATA * pMetaData = NULL;
W3_URL_INFO * pUrlInfo = NULL;
HRESULT hr;
if ( pW3Context == NULL ||
ppUrlInfo == NULL )
{
DBG_ASSERT( FALSE );
return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER );
}
*ppUrlInfo = NULL;
//
// Find a metacache entry
//
DBG_ASSERT( g_pW3Server->QueryMetaCache() != NULL );
hr = g_pW3Server->QueryMetaCache()->GetMetaData( pW3Context,
strUrl,
&pMetaData );
if ( FAILED( hr ) )
{
//
// Check for RPC disconnected error. If that's what happened,
// mark unhealthy.
//
// Oh yeah, make sure we only log one message
//
if ( hr == RPC_E_DISCONNECTED ||
hr == HRESULT_FROM_WIN32( RPC_S_SERVER_UNAVAILABLE ) ||
hr == HRESULT_FROM_WIN32( RPC_S_CALL_FAILED ) ||
hr == HRESULT_FROM_WIN32( RPC_S_CALL_FAILED_DNE ) )
{
if ( InterlockedExchange( (LPLONG) &_fMarkedUnhealthy, TRUE ) == FALSE )
{
g_pW3Server->LogEvent( W3_EVENT_NO_METABASE,
0,
NULL,
0 );
UlAtqSetUnhealthy();
}
}
return hr;
}
DBG_ASSERT( pMetaData != NULL );
//
// Create a W3_URL_INFO
//
pUrlInfo = new W3_URL_INFO( this, pMetaData );
if ( pUrlInfo == NULL )
{
hr = HRESULT_FROM_WIN32( GetLastError() );
pMetaData->DereferenceCacheEntry();
return hr;
}
hr = pUrlInfo->Create( strUrl,
strMetadataPath );
if ( FAILED( hr ) )
{
pUrlInfo->DereferenceCacheEntry();
return hr;
}
*ppUrlInfo = pUrlInfo;
return NO_ERROR;
}