369 lines
13 KiB
Plaintext
369 lines
13 KiB
Plaintext
/*++
|
|
|
|
Copyright (c) 1999 Microsoft Corporation
|
|
|
|
Module Name :
|
|
|
|
get.cool
|
|
|
|
Abstract:
|
|
|
|
This module implements the GET verb for static files in ASP.
|
|
|
|
Author:
|
|
|
|
Saurab Nog ( SaurabN ) 14-Apr-1999
|
|
|
|
Environment:
|
|
|
|
COM+ - User Mode Managed Run Time
|
|
|
|
Project:
|
|
|
|
Web Server
|
|
|
|
--*/
|
|
|
|
|
|
using System.IO;
|
|
using Microsoft.Win32;
|
|
using System.Collections;
|
|
using System.ASP;
|
|
using System.IIS.PrivateUtils;
|
|
|
|
namespace System.IIS
|
|
{
|
|
public class GetHandler : IHttpHandler
|
|
{
|
|
private const int DEFAULT_CACHE_THRESHOLD = 256*1024;
|
|
|
|
static public void CacheValidateHandler( HttpContext context,
|
|
Object data,
|
|
ref int validationStatus )
|
|
{
|
|
if ( context.Request.Headers[ "Range" ] != null ||
|
|
context.Request.RequestType.Equals( "(GETSOURCE)" ) ||
|
|
context.Request.RequestType.Equals( "(HEADSOURCE)" ) )
|
|
{
|
|
validationStatus = HttpValidationStatus.IgnoreThisRequest;
|
|
}
|
|
}
|
|
|
|
public void ProcessRequest( HttpContext context )
|
|
{
|
|
bool fExists = false;
|
|
FileEnumerator fe;
|
|
HttpRequest request = context.Request;
|
|
HttpResponse response = context.Response;
|
|
HttpUrl TargetUrl = request.Url;
|
|
string FileName = request.PhysicalPath;
|
|
|
|
Util.Debug.Trace( "GET", "URL = " + TargetUrl.ToString() );
|
|
Util.Debug.Trace( "GET", "File Name = " + FileName );
|
|
|
|
//
|
|
// Find the file using a file enumerator.
|
|
//
|
|
|
|
try
|
|
{
|
|
fe = new FileEnumerator( FileName );
|
|
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" );
|
|
}
|
|
|
|
//
|
|
// To be consistent with IIS, we won't serve out hidden files
|
|
//
|
|
|
|
if ( ( fe.Attributes & (int)FileAttributes.Hidden ) != 0 )
|
|
{
|
|
throw new HttpException( HttpStatus.NotFound,
|
|
"File is hidden" );
|
|
}
|
|
|
|
//
|
|
// BUGBUG: This is a hack to prevent the trailing dot problem.
|
|
// For now, error out all file names with trailing dot.
|
|
//
|
|
|
|
if ( FileName[ FileName.Length - 1 ] == '.' )
|
|
{
|
|
throw new HttpException( HttpStatus.NotFound,
|
|
"File does not exist" );
|
|
}
|
|
|
|
//
|
|
// If the file is a directory, then it must not have a slash in
|
|
// end of it (if it does have a slash suffix, then the config file
|
|
// mappings are missing and we will just return 403. Otherwise,
|
|
// we will redirect the client to the URL with this slash.
|
|
//
|
|
|
|
if ( ( fe.Attributes & (int)FileAttributes.Directory ) != 0 )
|
|
{
|
|
string strProtocol;
|
|
|
|
HttpUrl url;
|
|
|
|
if ( TargetUrl.Path.EndsWith( "/" ) )
|
|
{
|
|
//
|
|
// Just return 403
|
|
//
|
|
|
|
throw new HttpException( HttpStatus.Forbidden,
|
|
"Missing */ handler mapping. Cannot handle directory" );
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Redirect to a slash suffixed URL which will be
|
|
// handled by the */ handler mapper
|
|
//
|
|
|
|
strProtocol = request.IsSecureConnection ? "https" : "http";
|
|
|
|
url = new HttpUrl( strProtocol,
|
|
request.Url.Host,
|
|
request.Url.Port,
|
|
TargetUrl.Path + "/",
|
|
null,
|
|
null );
|
|
|
|
response.Redirect( url.ToString() );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DateTime lastModified;
|
|
string strETag;
|
|
|
|
//
|
|
// Determine Last Modified Time. We might need it soon
|
|
// if we encounter a Range: and If-Range header
|
|
//
|
|
|
|
lastModified = CacheValidation.RoundDateTime( fe.LastWriteTime );
|
|
|
|
//
|
|
// Generate ETag
|
|
//
|
|
|
|
strETag = CacheValidation.GenerateETag( context,
|
|
lastModified );
|
|
|
|
//
|
|
// OK. Send the static file out either
|
|
// entirely or send out the requested ranges
|
|
//
|
|
|
|
try
|
|
{
|
|
BuildFileItemResponse( context,
|
|
FileName,
|
|
fe.Size,
|
|
lastModified,
|
|
strETag );
|
|
}
|
|
catch ( IOException ex )
|
|
{
|
|
//
|
|
// Check for ERROR_ACCESS_DENIED and set the HTTP
|
|
// status such that the auth modules do their thing
|
|
//
|
|
|
|
if ( GeneralSecUtils.IsSecurityError( ex.ErrorCode ) )
|
|
{
|
|
throw new HttpException( HttpStatus.Unauthorized,
|
|
"Resource Access Forbidden" );
|
|
}
|
|
}
|
|
|
|
context.Response.Cache.SetLastModified( lastModified );
|
|
|
|
context.Response.Cache.SetETag( strETag );
|
|
|
|
//
|
|
// We will always set Cache-Control to public
|
|
//
|
|
|
|
context.Response.Cache.SetCacheability( HttpCacheability.Public );
|
|
}
|
|
}
|
|
|
|
void BuildFileItemResponse( HttpContext context,
|
|
string fileName,
|
|
long fileSize,
|
|
DateTime lastModifiedTime,
|
|
string strETag )
|
|
{
|
|
HttpRequest request = context.Request;
|
|
HttpResponse response = context.Response;
|
|
bool fCache = false;
|
|
string strRange;
|
|
int cbCacheThreshold = DEFAULT_CACHE_THRESHOLD;
|
|
Dictionary cacheOptions;
|
|
bool fIsRangeRequest = false;
|
|
|
|
//
|
|
// Get the Range: header if it exists
|
|
//
|
|
|
|
strRange = request.Headers[ "Range" ];
|
|
if ( strRange != null )
|
|
{
|
|
if ( strRange.ToLower().StartsWith( "bytes" ) )
|
|
{
|
|
fIsRangeRequest = true;
|
|
}
|
|
}
|
|
|
|
//
|
|
// BUGBUG - Enable this
|
|
//
|
|
// _response.Flags = (UL_HTTP_RESPONSE_FLAG_CALC_CONTENT_LENGTH |
|
|
// UL_HTTP_RESPONSE_FLAG_CALC_ETAG |
|
|
// UL_HTTP_RESPONSE_FLAG_CALC_LAST_MODIFIED);
|
|
//
|
|
|
|
//
|
|
// Give the range code a first crack at sending the ranges. If
|
|
// the Range: header is syntactically invalid, then we will fall
|
|
// thru as if the Range: header was not present.
|
|
//
|
|
|
|
if ( fIsRangeRequest &&
|
|
!CacheValidation.SendEntireEntity( context,
|
|
strETag,
|
|
lastModifiedTime ) )
|
|
{
|
|
// At this point we know that based on "If-Range"
|
|
// (if provided) we may not send the entire entity.
|
|
|
|
if ( RangeSupport.ProcessRangeRequest( context,
|
|
strRange,
|
|
fileName,
|
|
fileSize ) )
|
|
{
|
|
//
|
|
// If ProcessRangeRequest() returned true, then it
|
|
// handled the range somehow (either sending it back
|
|
// with a 206 or sent a 416
|
|
//
|
|
|
|
response.Cache.SetNoServerCaching();
|
|
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Fall thru. The request is now cacheable again
|
|
//
|
|
}
|
|
|
|
//
|
|
// Get the threshold for caching if there is a chance we could
|
|
// cache this
|
|
//
|
|
|
|
cacheOptions = (Dictionary) context.GetConfig( "CacheOptions" );
|
|
if ( cacheOptions != null )
|
|
{
|
|
string strThreshold;
|
|
|
|
strThreshold = (string) cacheOptions[ "ServerSideCacheThreshold" ];
|
|
if ( strThreshold != null )
|
|
{
|
|
cbCacheThreshold = int.FromString( strThreshold );
|
|
}
|
|
}
|
|
|
|
if ( fileSize <= cbCacheThreshold &&
|
|
!request.RequestType.Equals( "(GETSOURCE)" ) &&
|
|
!request.RequestType.Equals( "(HEADSOURCE)" ) )
|
|
{
|
|
fCache = true;
|
|
}
|
|
|
|
//
|
|
// Ask ASP to open the file contents and cache them
|
|
// (hence the second parameter to WriteFile())
|
|
//
|
|
|
|
response.WriteFile( fileName, fCache );
|
|
|
|
//
|
|
// Specify content type. Use extension to do the mapping
|
|
//
|
|
|
|
response.ContentType = MimeMapping.GetMimeMapping( fileName );
|
|
|
|
//
|
|
// Static file handler supports byte ranges (duh)
|
|
//
|
|
|
|
response.AppendHeader( "Accept-Ranges", "bytes" );
|
|
|
|
//
|
|
// If we are caching, the instruct the ASP output cache to
|
|
// to cache the result.
|
|
//
|
|
|
|
if ( fCache )
|
|
{
|
|
//
|
|
// Set a validation handler to check to avoid serving from
|
|
// XSP output cache when Range or Translate:f
|
|
//
|
|
|
|
response.Cache.AddValidationCallback(
|
|
new HttpCacheValidateHandler( CacheValidateHandler ),
|
|
null );
|
|
|
|
//
|
|
//
|
|
// We want to flush cache entry when static file has changed
|
|
//
|
|
|
|
response.AddFileDependency( fileName );
|
|
|
|
//
|
|
// Set an expires in the future since XSP cache module
|
|
// expects one. BUGBUG: They really shouldn't since
|
|
// proxies will use heuristics to produce an expires based
|
|
// off last modified time
|
|
//
|
|
|
|
response.Cache.SetExpires( DateTime.Now.AddDays( 1 ) );
|
|
}
|
|
}
|
|
|
|
public bool IsReusable()
|
|
{
|
|
return true;
|
|
}
|
|
};
|
|
}
|