// -----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// -----------------------------------------------------------------------
namespace Microsoft.Samples.Kinect.Webserver
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Threading;
using System.Windows.Threading;
using Microsoft.Kinect.Toolkit;
///
/// HTTP request/response handler that listens for and processes HTTP requests in
/// a thread dedicated to that single purpose.
///
public sealed class ThreadHostedHttpListener
{
///
/// URI origins for which server is expected to be listening.
///
private readonly List ownedOriginUris = new List();
///
/// Origin Uris that are allowed to access data served by this listener.
///
private readonly HashSet allowedOriginUris = new HashSet();
///
/// Mapping between URI paths and factories of request handlers that can correspond
/// to them.
///
private readonly Dictionary requestHandlerFactoryMap;
///
/// SynchronizationContext wrapper used to track event handlers for Started event.
///
private readonly ContextEventWrapper startedContextWrapper =
new ContextEventWrapper(ContextSynchronizationMethod.Post);
///
/// SynchronizationContext wrapper used to track event handlers for Stopped event.
///
private readonly ContextEventWrapper stoppedContextWrapper =
new ContextEventWrapper(ContextSynchronizationMethod.Post);
///
/// Object used to synchronize access to data shared between client calling thread(s)
/// and listener thread.
///
private readonly object lockObject = new object();
///
/// Data shared between client calling thread(s) and listener thread.
///
private SharedThreadData threadData;
///
/// Initializes a new instance of the ThreadHostedHttpListener class.
///
///
/// URI origins for which server is expected to be listening.
///
///
/// Origin Uris that are allowed to access data served by this listener, in addition
/// to owned origin Uris. May be empty or null if only owned origins are allowed to
/// access server data.
///
///
/// Mapping between URI paths and factories of request handlers that can correspond
/// to them.
///
public ThreadHostedHttpListener(IEnumerable ownedOrigins, IEnumerable allowedOrigins, Dictionary requestHandlerFactoryMap)
{
if (ownedOrigins == null)
{
throw new ArgumentNullException("ownedOrigins");
}
if (requestHandlerFactoryMap == null)
{
throw new ArgumentNullException("requestHandlerFactoryMap");
}
this.requestHandlerFactoryMap = requestHandlerFactoryMap;
foreach (var origin in ownedOrigins)
{
this.ownedOriginUris.Add(origin);
this.allowedOriginUris.Add(origin);
}
if (allowedOrigins != null)
{
foreach (var origin in allowedOrigins)
{
this.allowedOriginUris.Add(origin);
}
}
}
///
/// Event used to signal that the server has started listening for connections.
///
public event EventHandler Started
{
add { this.startedContextWrapper.AddHandler(value); }
remove { this.startedContextWrapper.RemoveHandler(value); }
}
///
/// Event used to signal that the server has stopped listening for connections.
///
public event EventHandler Stopped
{
add { this.stoppedContextWrapper.AddHandler(value); }
remove { this.stoppedContextWrapper.RemoveHandler(value); }
}
///
/// True if listener has a thread actively listening for HTTP requests.
///
public bool IsListening
{
get
{
return (this.threadData != null) && this.threadData.Thread.IsAlive;
}
}
///
/// Start listening for requests.
///
public void Start()
{
lock (this.lockObject)
{
Thread oldThread = null;
// If thread is currently running
if (this.IsListening)
{
if (!this.threadData.StopRequestSent)
{
// Thread is already running and ready to handle requests, so there
// is no need to start up a new one.
return;
}
// If thread is running, but still in the process of winding down,
// dissociate server from currently running thread without waiting for
// thread to finish.
// New thread will wait for previous thread to finish so that there is
// no conflict between two different threads listening on the same URI
// prefixes.
oldThread = this.threadData.Thread;
this.threadData = null;
}
this.threadData = new SharedThreadData
{
Thread = new Thread(this.ListenerThread),
PreviousThread = oldThread
};
this.threadData.Thread.Start(this.threadData);
}
}
///
/// Stop listening for requests, optionally waiting for thread to finish.
///
///
/// True if caller wants to wait until listener thread has terminated before
/// returning.
/// False otherwise.
///
public void Stop(bool wait)
{
Thread listenerThread = null;
lock (this.lockObject)
{
if (this.IsListening)
{
if (!this.threadData.StopRequestSent)
{
// Request the thread to end, but keep remembering the old thread
// data in case listener gets re-started immediately and new thread
// has to wait for old thread to finish before getting started.
SendStopMessage(this.threadData);
}
if (wait)
{
listenerThread = this.threadData.Thread;
}
}
}
if (listenerThread != null)
{
listenerThread.Join();
}
}
///
/// Stop listening for requests but don't wait for thread to finish.
///
public void Stop()
{
this.Stop(false);
}
///
/// Let listener thread know that it should wind down and stop listening for incoming
/// HTTP requests.
///
///
/// Object containing shared data used to communicate with listener thread.
///
private static void SendStopMessage(SharedThreadData threadData)
{
if (threadData.StateManager != null)
{
// If there's already a state manager associated with the server thread,
// tell it to stop listening.
threadData.StateManager.Stop();
}
// Even if there's no dispatcher associated with the server thread yet,
// let thread know that it should bail out immediately after starting
// up, and never push a dispatcher frame at all.
threadData.StopRequestSent = true;
}
///
/// Thread procedure for HTTP listener thread.
///
///
/// Object containing shared data used to communicate between client thread
/// and listener thread.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Main listener thread should never crash. Errors are captured in web server trace intead.")]
private void ListenerThread(object data)
{
var sharedData = (SharedThreadData)data;
lock (this.lockObject)
{
if (sharedData.StopRequestSent)
{
return;
}
sharedData.StateManager = new ListenerThreadStateManager(this.ownedOriginUris, this.allowedOriginUris, this.requestHandlerFactoryMap);
}
try
{
if (sharedData.PreviousThread != null)
{
// Wait for the previous thread to finish so that only one thread can be listening
// on the specified prefixes at any one time.
sharedData.PreviousThread.Join();
sharedData.PreviousThread = null;
}
//// After this point, it is expected that the only mutation of shared data triggered by
//// another thread will be to signal that this thread should stop listening.
this.startedContextWrapper.Invoke(this, EventArgs.Empty);
sharedData.StateManager.Listen();
}
catch (Exception e)
{
Trace.TraceError("Exception encountered while listening for connections:\n{0}", e);
}
finally
{
sharedData.StateManager.Dispose();
this.stoppedContextWrapper.Invoke(this, EventArgs.Empty);
}
}
///
/// Represents state that needs to be shared between listener thread and client calling thread.
///
private sealed class SharedThreadData
{
///
/// Listener thread.
///
public Thread Thread { get; set; }
///
/// Object used to manage state and safely communicate with listener thread.
///
public ListenerThreadStateManager StateManager { get; set; }
///
/// True if the specific listener thread associated with this shared data
/// has previously been requested to stop.
/// False otherwise.
///
public bool StopRequestSent { get; set; }
///
/// Previous listener thread, which had not fully finished processing before
/// a new thread was requested. Most of the time this will be null.
///
public Thread PreviousThread { get; set; }
}
///
/// Manages state used by a single listener thread.
///
private sealed class ListenerThreadStateManager : IDisposable
{
///
/// URI origins for which server is expected to be listening.
///
private readonly List ownedOriginUris;
///
/// Origin Uris that are allowed to access data served by this listener.
///
private readonly HashSet allowedOriginUris;
///
/// HttpListener used to wait for incoming HTTP requests.
///
private readonly HttpListener listener = new HttpListener();
///
/// Mapping between URI paths and factories of request handlers that can correspond
/// to them.
///
private readonly Dictionary requestHandlerFactoryMap = new Dictionary();
///
/// Mapping between URI paths and corresponding request handlers.
///
private readonly Dictionary requestHandlerMap = new Dictionary();
///
/// Dispatcher used to manage the queue of work done in listener thread.
///
private readonly Dispatcher dispatcher;
///
/// Represents main execution loop in listener thread.
///
private readonly DispatcherFrame frame;
///
/// Asynchronous result indicating that we're currently waiting for some HTTP
/// client to initiate a request.
///
private IAsyncResult getContextResult;
///
/// Initializes a new instance of the ListenerThreadStateManager class.
///
///
/// URI origins for which server is expected to be listening.
///
///
/// URI origins that are allowed to access data served by this listener.
///
///
/// Mapping between URI paths and factories of request handlers that can correspond
/// to them.
///
internal ListenerThreadStateManager(List ownedOriginUris, HashSet allowedOriginUris, Dictionary requestHandlerFactoryMap)
{
this.ownedOriginUris = ownedOriginUris;
this.allowedOriginUris = allowedOriginUris;
this.dispatcher = Dispatcher.CurrentDispatcher;
this.frame = new DispatcherFrame { Continue = true };
this.requestHandlerFactoryMap = requestHandlerFactoryMap;
}
///
/// Releases resources used while listening for HTTP requests.
///
public void Dispose()
{
this.listener.Close();
}
internal void Stop()
{
this.dispatcher.BeginInvoke((Action)(() =>
{
foreach (var handler in this.requestHandlerMap)
{
handler.Value.Cancel();
}
this.frame.Continue = false;
}));
}
///
/// Initializes request handlers, listens for incoming HTTP requests until client
/// requests us to stop and then uninitializes request handlers.
///
internal void Listen()
{
foreach (var entry in this.requestHandlerFactoryMap)
{
var path = entry.Key;
// To simplify lookup against "PathAndQuery" property of Uri objects,
// we ensure that this has the starting forward slash that PathAndQuery
// property values have.
if (!path.StartsWith("/", StringComparison.OrdinalIgnoreCase))
{
path = "/" + path;
}
// Listen for each handler path under each origin
foreach (var originUri in this.ownedOriginUris)
{
// HttpListener only listens to URIs that end in "/", but also remember
// path exactly as requested by client associated with request handler,
// to match subpath expressions expected by handler
var uriBuilder = new UriBuilder(originUri) { Path = path };
var prefix = uriBuilder.ToString();
if (!prefix.EndsWith("/", StringComparison.OrdinalIgnoreCase))
{
prefix = prefix + "/";
}
listener.Prefixes.Add(prefix);
}
requestHandlerMap.Add(path, entry.Value.CreateHandler());
}
this.listener.Start();
try
{
var initialize = (Action)(async () =>
{
foreach (var handler in requestHandlerMap.Values)
{
await handler.InitializeAsync();
}
this.getContextResult = this.listener.BeginGetContext(this.GetContextCallback, null);
});
this.dispatcher.BeginInvoke(DispatcherPriority.Normal, initialize);
Dispatcher.PushFrame(this.frame);
var uninitializeFrame = new DispatcherFrame { Continue = true };
var uninitialize = (Action)(async () =>
{
foreach (var handler in this.requestHandlerMap.Values)
{
await handler.UninitializeAsync();
}
uninitializeFrame.Continue = false;
});
this.dispatcher.BeginInvoke(DispatcherPriority.Normal, uninitialize);
Dispatcher.PushFrame(uninitializeFrame);
}
finally
{
this.listener.Stop();
}
}
///
/// Close response stream and associate a status code with response.
///
///
/// Context whose response we should close.
///
///
/// Status code.
///
private static void CloseResponse(HttpListenerContext context, HttpStatusCode statusCode)
{
try
{
context.Response.StatusCode = (int)statusCode;
context.Response.Close();
}
catch (HttpListenerException e)
{
Trace.TraceWarning(
"Problem encountered while sending response for kinect sensor request. Client might have aborted request. Cause: \"{0}\"", e.Message);
}
}
///
/// Checks if this is corresponds to a cross-origin request and, if so, prepares
/// the response with the appropriate headers or even body, if necessary.
///
///
/// Listener context containing the request and associated response object.
///
///
/// True if request was initiated by an explicitly allowed origin URI.
/// False if origin URI for request is not allowed.
///
private bool HandleCrossOrigin(HttpListenerContext context)
{
const string OriginHeader = "Origin";
const string AllowOriginHeader = "Access-Control-Allow-Origin";
const string RequestHeadersHeader = "Access-Control-Request-Headers";
const string AllowHeadersHeader = "Access-Control-Allow-Headers";
const string RequestMethodHeader = "Access-Control-Request-Method";
const string AllowMethodHeader = "Access-Control-Allow-Methods";
// Origin header is not required, since it is up to browser to
// detect when cross-origin security checks are needed.
var originValue = context.Request.Headers[OriginHeader];
if (originValue != null)
{
// If origin header is present, check if it's in allowed list
Uri originUri;
try
{
originUri = new Uri(originValue);
}
catch (UriFormatException)
{
return false;
}
if (!this.allowedOriginUris.Contains(originUri))
{
return false;
}
// We allow all origins to access this server's data
context.Response.Headers.Add(AllowOriginHeader, originValue);
}
var requestHeaders = context.Request.Headers[RequestHeadersHeader];
if (requestHeaders != null)
{
// We allow all headers in cross-origin server requests
context.Response.Headers.Add(AllowHeadersHeader, requestHeaders);
}
var requestMethod = context.Request.Headers[RequestMethodHeader];
if (requestMethod != null)
{
// We allow all methods in cross-origin server requests
context.Response.Headers.Add(AllowMethodHeader, requestMethod);
}
return true;
}
///
/// Callback used by listener to let us know when we have received an HTTP
/// context corresponding to an earlier call to BeginGetContext.
///
///
/// Status of asynchronous operation.
///
private void GetContextCallback(IAsyncResult result)
{
this.dispatcher.BeginInvoke((Action)(() =>
{
if (!this.listener.IsListening)
{
return;
}
Debug.Assert(result == this.getContextResult, "remembered GetContext result should match result handed to callback.");
var httpListenerContext = this.listener.EndGetContext(result);
this.getContextResult = null;
this.HandleRequestAsync(httpListenerContext);
if (this.frame.Continue)
{
this.getContextResult = this.listener.BeginGetContext(this.GetContextCallback, null);
}
}));
}
///
/// Handle an HTTP request asynchronously
///
///
/// Context containing HTTP request and response information
///
private async void HandleRequestAsync(HttpListenerContext httpListenerContext)
{
var uri = httpListenerContext.Request.Url;
var clientAddress = httpListenerContext.Request.RemoteEndPoint != null
? httpListenerContext.Request.RemoteEndPoint.Address
: IPAddress.None;
var requestOverview = string.Format("URI=\"{0}\", client=\"{1}\"", uri, clientAddress);
try
{
bool foundHandler = false;
if (!this.HandleCrossOrigin(httpListenerContext))
{
CloseResponse(httpListenerContext, HttpStatusCode.Forbidden);
}
else
{
foreach (var entry in this.requestHandlerMap)
{
if (uri.PathAndQuery.StartsWith(entry.Key, StringComparison.InvariantCultureIgnoreCase))
{
foundHandler = true;
var subPath = uri.PathAndQuery.Substring(entry.Key.Length);
await entry.Value.HandleRequestAsync(httpListenerContext, subPath);
break;
}
}
if (!foundHandler)
{
CloseResponse(httpListenerContext, HttpStatusCode.NotFound);
}
}
Trace.TraceInformation("Request for {0} completed with result: {1}", requestOverview, httpListenerContext.Response.StatusCode);
}
catch (Exception e)
{
Trace.TraceError("Exception encountered while handling request for {0}:\n{1}", requestOverview, e);
CloseResponse(httpListenerContext, HttpStatusCode.InternalServerError);
}
}
}
}
}