WindowsXP/inetsrv/iis/iisrearc/core/webserver/cachevalidation.cool
2025-04-27 07:49:33 -04:00

544 lines
18 KiB
Plaintext

/*++
Copyright (c) 1999 Microsoft Corporation
Module Name :
CacheValidation.cool
Abstract:
Utilities for ETag generation and If- header evaluation
Author:
Bilal Alam (BAlam) 26-Oct-99
Environment:
COM+ - User Mode Managed Run Time
Project:
Web Server
--*/
using System;
using System.Interop;
using System.ASP;
using System.Collections;
using System.IIS.Hosting;
using System.IO;
namespace System.IIS
{
[permissionset(System.Security.SecurityManager.DeclCheck, System.Security.Capability.Nothing)]
internal class CacheValidation
{
[sysimport(dll="IISWP.EXE")]
private static extern bool StringTimeToFileTime(
[nativetype(NativeType.NativeTypeLpwstr)] string strUrlFlush,
out long liFileTime
);
//
// Compare equality of two ETags
//
private static bool CompareETags( string strETag1,
string strETag2 )
{
bool fMatch = false;
if ( strETag1.Equals( "*" ) || strETag2.Equals( "*" ) )
{
fMatch = true;
goto Finished;
}
if ( strETag1.StartsWith( "W/" ) )
{
strETag1 = strETag1.Substring( 2 );
}
if ( strETag2.StartsWith( "W/" ) )
{
strETag2 = strETag2.Substring( 2 );
}
fMatch = strETag2.Equals( strETag1 );
Finished:
return fMatch;
}
//
// From a list of comma delimited ETags, generate an array list
// of those ETags
//
private static ArrayList GetETagList( string strList )
{
string strRemaining;
string strETag;
int iIndex;
ArrayList arrayRet = null;
strRemaining = strList;
do
{
iIndex = strRemaining.IndexOf( ',' );
if ( iIndex == -1 )
{
strETag = strRemaining.Trim();
}
else
{
strETag = strRemaining.Substring( 0, iIndex ).Trim();
strRemaining = strRemaining.Substring( iIndex + 1 );
}
//
// Add strETag
//
if ( arrayRet == null )
{
arrayRet = new ArrayList( 1 );
}
arrayRet.Add( strETag );
}
while ( iIndex != -1 );
return arrayRet;
}
//
// Validate the date. In particular, any date later than now (
// now determined by current time) is considered invalid
//
private static bool ValidDate( DateTime dateTime )
{
return DateTime.Compare( DateTime.Now, dateTime ) > 0;
}
//
// Handle the If-* verbs given an ETag and last mod time
//
private static int HandleValidation( HttpContext context,
string strETag,
DateTime lastModifiedTime )
{
string strValidation;
int RetStatus = HttpStatus.Ok;
//
// If-Match
//
strValidation = context.Request.Headers[ "If-Match" ];
if ( strValidation != null )
{
ArrayList strMatchETags;
bool fMatch = false;
if ( strETag == null || strETag.StartsWith( "W/" ) )
{
fMatch = true;
}
else
{
strMatchETags = GetETagList( strValidation );
if ( strMatchETags != null )
{
for( int i = 0; i < strMatchETags.Count; i++ )
{
if ( CompareETags( strETag, (string) strMatchETags[ i ] ) )
{
fMatch = true;
break;
}
}
}
}
if ( !fMatch )
{
return HttpStatus.PreconditionFailed;
}
}
//
// If-Unmodified-Since
//
strValidation = context.Request.Headers[ "If-Unmodified-Since" ];
if ( strValidation != null )
{
long liFileTime = 0;
//
// Parse header and get response
//
if ( StringTimeToFileTime( strValidation, out liFileTime ) )
{
DateTime modifiedTime;
modifiedTime = DateTime.FromFileTime( liFileTime );
if ( ( lastModifiedTime == DateTime.MinValue ||
DateTime.Compare( lastModifiedTime, modifiedTime ) == 1 ) &&
ValidDate( modifiedTime ) )
{
return HttpStatus.PreconditionFailed;
}
}
}
//
// If-None-Match
//
strValidation = context.Request.Headers[ "If-None-Match" ];
if ( strValidation != null )
{
ArrayList strMatchETags;
bool fMatch = false;
bool fWeakCompare;
fWeakCompare = context.Request.Headers[ "Range" ] == null;
if ( strETag != null &&
!( !fWeakCompare &&
strETag.StartsWith( "W/" ) ) )
{
strMatchETags = GetETagList( strValidation );
if ( strMatchETags != null )
{
for( int i = 0; i < strMatchETags.Count; i++ )
{
if ( CompareETags( strETag, (string) strMatchETags[ i ] ) )
{
fMatch = true;
break;
}
}
}
if ( !fMatch )
{
return RetStatus;
}
else
{
if ( context.Request.HttpMethod.Equals( "GET" ) ||
context.Request.HttpMethod.Equals( "HEAD" ) )
{
RetStatus = HttpStatus.NotModified;
}
else
{
return HttpStatus.PreconditionFailed;
}
}
}
}
//
// If-Modified-Since
//
strValidation = context.Request.Headers[ "If-Modified-Since" ];
if ( strValidation != null )
{
long liFileTime = 0;
//
// Parse header and get response
//
if ( StringTimeToFileTime( strValidation, out liFileTime ) )
{
DateTime modifiedTime;
int iDateCompare;
modifiedTime = DateTime.FromFileTime( liFileTime );
iDateCompare = DateTime.Compare( lastModifiedTime,
modifiedTime );
if ( ( lastModifiedTime == DateTime.MinValue ||
iDateCompare == -1 ||
iDateCompare == 0 ) &&
ValidDate( modifiedTime ) )
{
if ( context.Request.HttpMethod.Equals( "GET" ) ||
context.Request.HttpMethod.Equals( "HEAD" ) )
{
RetStatus = HttpStatus.NotModified;
}
else
{
RetStatus = HttpStatus.PreconditionFailed;
}
}
else
{
if( RetStatus == HttpStatus.NotModified )
{
RetStatus = HttpStatus.Ok;
}
}
}
}
return RetStatus;
}
//
// Generate an E-Tag for response
//
public static string GenerateETag( HttpContext context,
DateTime lastModTime )
{
HttpWorkerRequest workerRequest;
long appDomainFileTime;
long lastModFileTime;
StringBuilder strETag = new StringBuilder();
//
// For now, we will produce an incredibly lame ETag which
// will be invalidated when:
// a) The static file has changes (that's ok)
// b) When an apppool starts
//
workerRequest = (HttpWorkerRequest) context.GetServiceObject(
typeof( HttpWorkerRequest ) );
if ( workerRequest is ULWorkerRequest )
{
ULWorkerRequest ulWorkerRequest;
ulWorkerRequest = (ULWorkerRequest) workerRequest;
appDomainFileTime = ulWorkerRequest.AppDomainCreateFileTime;
}
else
{
appDomainFileTime = DateTime.Now.ToFileTime();
}
//
// Get 64-bit FILETIME stamps
//
lastModFileTime = lastModTime.ToFileTime();
//
// ETag is "<hexified last mod>:<hexified create app>"
//
strETag.Append( "\"" );
strETag.Append( long.Format( lastModFileTime, "X8" ) );
strETag.Append( ":" );
strETag.Append( long.Format( appDomainFileTime, "X8" ) );
strETag.Append( "\"" );
//
// Is this a strong ETag. Do what IIS does to determine this.
// Compare the last modified time to now and if it earlier by
// more than 3 seconds, then it is strong.Equals( strIfRange ) )
//
// BUGBUG: Bug JohnL about why this works?
//
if ( !( ( DateTime.Now.ToFileTime() - lastModFileTime ) > 30000000 ) )
{
//
// Weak ETag
//
return "W/" + strETag.ToString();
}
else
{
//
// Stron ETag. Leave as is
//
return strETag.ToString();
}
}
//
// Round a datetime to the nearest second.
// BUGBUG: There has to be a better way.
//
public static DateTime RoundDateTime( DateTime dateTime )
{
return new DateTime( dateTime.Year,
dateTime.Month,
dateTime.Day,
dateTime.Hour,
dateTime.Minute,
dateTime.Second,
0 );
}
public static int Validate( HttpContext context )
{
HttpCachePolicySettings cacheSettings;
cacheSettings = context.Response.Cache.GetCurrentSettings();
return Validate( context,
cacheSettings.ETag,
cacheSettings.LastModified );
}
public static int Validate( HttpContext context,
string strFileName )
{
FileEnumerator fe;
bool fExists = false;
string strETag;
DateTime dateTime;
//
// Find the file using a file enumerator.
//
try
{
fe = new FileEnumerator( strFileName );
fExists = fe.GetNext();
}
catch ( IOException ioEx )
{
throw new HttpException( HttpStatus.NotFound,
"Error trying to enumerate files",
ioEx );
}
catch ( SecurityException secEx )
{
throw new HttpException( HttpStatus.Unauthorized,
"File enumerator access denied",
secEx );
}
//
// Check whether the file exists
//
if ( !fExists )
{
throw new HttpException( HttpStatus.NotFound,
"File does not exist" );
}
//
// Generate ETag
//
dateTime = RoundDateTime( fe.LastWriteTime );
strETag = GenerateETag( context, dateTime );
return Validate( context,
strETag,
dateTime );
}
public static int Validate( HttpContext context,
string strETag,
DateTime dateTime )
{
return HandleValidation( context,
strETag,
dateTime );
}
public static bool SendEntireEntity( HttpContext context,
string strETag,
DateTime lastModifiedTime )
{
string strIfRange;
bool fEntireEntity = false;
//
// The only way we would not send the range (and instead send
// the entire entity) is if the If-Range header did not hold
//
//
// NOTE: This routine is called by range handling code which
// would have first verified that there is indeed a
// Range: header in the request.
//
strIfRange = context.Request.Headers[ "If-Range" ];
if ( strIfRange == null )
{
return false;
}
else
{
//
// Is this an ETag or a Date?
// -- entity-tags are quoted strings, HTTP-dates are not
//
if ( strIfRange[ 0 ] == '"' )
{
//
// ETag
//
if ( !CompareETags( strIfRange, strETag ) )
{
fEntireEntity = true;
}
}
else
{
//
// Date
//
long liFileTime = 0;
if ( StringTimeToFileTime( strIfRange,
out liFileTime ) )
{
int iDateCompare;
DateTime modifiedTime;
modifiedTime = DateTime.FromFileTime( liFileTime );
iDateCompare = DateTime.Compare( lastModifiedTime,
modifiedTime );
if ( iDateCompare == 1 )
{
fEntireEntity = true;
}
}
else
{
fEntireEntity = true;
}
}
}
return fEntireEntity;
}
}
}