// -----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// -----------------------------------------------------------------------
namespace Microsoft.Samples.Kinect.Webserver
{
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using System.Web;
///
/// Implementation of IHttpRequestHandler used to serve static file content.
///
public class FileRequestHandler : IHttpRequestHandler
{
///
/// Root directory in server's file system from which we're serving files.
///
private readonly DirectoryInfo rootDirectory;
///
/// Origin used as a helper for URI parsing and canonicalization.
///
private readonly Uri parseHelperOrigin = new Uri("http://a");
///
/// Initializes a new instance of the class.
///
///
/// Root directory in server's file system from which files should be served.
///
internal FileRequestHandler(DirectoryInfo rootDirectory)
{
if (rootDirectory == null)
{
throw new ArgumentNullException("rootDirectory");
}
// Re-create directory name to ensure equivalence between directory names
// that end in "\" character and directory names that are identical except
// that they don't end in "\" character.
var normalizedDirectoryName = rootDirectory.Parent != null
? Path.Combine(rootDirectory.Parent.FullName, rootDirectory.Name)
: rootDirectory.Name;
this.rootDirectory = new DirectoryInfo(normalizedDirectoryName);
}
///
/// Prepares handler to start receiving HTTP requests.
///
///
/// Await-able task.
///
///
/// Return value should never be null. Implementations should use Task.FromResult(0)
/// if function is implemented synchronously so that callers can await without
/// needing to check for null.
///
public Task InitializeAsync()
{
return SharedConstants.EmptyCompletedTask;
}
///
/// Handle an http request.
///
///
/// Context containing HTTP request data, which will also contain associated
/// response upon return.
///
///
/// Request URI path relative to the URI prefix associated with this request
/// handler in the HttpListener.
///
///
/// Await-able task.
///
///
/// Return value should never be null. Implementations should use Task.FromResult(0)
/// if function is implemented synchronously so that callers can await without
/// needing to check for null.
///
public async Task HandleRequestAsync(HttpListenerContext requestContext, string subpath)
{
if (requestContext == null)
{
throw new ArgumentNullException("requestContext");
}
if (subpath == null)
{
throw new ArgumentNullException("subpath");
}
try
{
var statusCode = await this.ServeFileAsync(requestContext.Response, subpath);
CloseResponse(requestContext, statusCode);
}
catch (HttpListenerException e)
{
Trace.TraceWarning(
"Problem encountered while serving file at subpath {0}. Client might have aborted request. Cause: \"{1}\"", subpath, e.Message);
}
}
///
/// Cancel all pending operations.
///
public void Cancel()
{
}
///
/// Lets handler know that no more HTTP requests will be received, so that it can
/// clean up resources associated with request handling.
///
///
/// Await-able task.
///
///
/// Return value should never be null. Implementations should use Task.FromResult(0)
/// if function is implemented synchronously so that callers can await without
/// needing to check for null.
///
public Task UninitializeAsync()
{
return SharedConstants.EmptyCompletedTask;
}
///
/// Close response stream and associate a status code with response.
///
///
/// Context whose response we should close.
///
///
/// Status code.
///
internal static void CloseResponse(HttpListenerContext context, HttpStatusCode statusCode)
{
context.Response.StatusCode = (int)statusCode;
context.Response.Close();
}
///
/// Determine if the specified directory is an ancestor of the specified file.
///
///
/// Information for potential ancestor directory.
///
///
/// File for which to determine directory ancestry.
///
///
/// True if is an ancestor of .
///
internal static bool IsAncestorDirectory(DirectoryInfo ancestorDirectory, FileInfo file)
{
var parentDirectory = file.Directory;
while (parentDirectory != null)
{
if (string.Compare(parentDirectory.FullName, ancestorDirectory.FullName, StringComparison.OrdinalIgnoreCase) == 0)
{
return true;
}
parentDirectory = parentDirectory.Parent;
}
return false;
}
///
/// Serve the file corresponding to the specified URI path.
///
///
/// HTTP response where file will be streamed, on success.
///
///
/// Request URI path relative to the URI prefix associated with this request
/// handler in the HttpListener.
///
///
/// Await-able task holding an HTTP status code that should be part of response.
///
private async Task ServeFileAsync(HttpListenerResponse response, string subpath)
{
Uri uri;
try
{
uri = new Uri(this.parseHelperOrigin, subpath);
}
catch (UriFormatException)
{
return HttpStatusCode.Forbidden;
}
var filePath = Path.Combine(this.rootDirectory.FullName, uri.AbsolutePath.Substring(1));
var fileInfo = new FileInfo(filePath);
if (!IsAncestorDirectory(this.rootDirectory, fileInfo))
{
return HttpStatusCode.Forbidden;
}
if (!fileInfo.Exists)
{
return HttpStatusCode.NotFound;
}
response.ContentType = MimeMapping.GetMimeMapping(fileInfo.FullName);
using (var fileStream = fileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
{
await fileStream.CopyToAsync(response.OutputStream);
}
return HttpStatusCode.OK;
}
}
}