// ----------------------------------------------------------------------- // // 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); } } } } }