source: other-projects/playing-in-the-street/summer-2013/trunk/Playing-in-the-Street-WPF/Microsoft.Samples.Kinect.Webserver/ThreadHostedHttpListener.cs@ 28897

Last change on this file since 28897 was 28897, checked in by davidb, 10 years ago

GUI front-end to server base plus web page content

File size: 26.4 KB
Line 
1// -----------------------------------------------------------------------
2// <copyright file="ThreadHostedHttpListener.cs" company="Microsoft">
3// Copyright (c) Microsoft Corporation. All rights reserved.
4// </copyright>
5// -----------------------------------------------------------------------
6
7namespace Microsoft.Samples.Kinect.Webserver
8{
9 using System;
10 using System.Collections.Generic;
11 using System.Diagnostics;
12 using System.Net;
13 using System.Threading;
14 using System.Windows.Threading;
15
16 using Microsoft.Kinect.Toolkit;
17
18 /// <summary>
19 /// HTTP request/response handler that listens for and processes HTTP requests in
20 /// a thread dedicated to that single purpose.
21 /// </summary>
22 public sealed class ThreadHostedHttpListener
23 {
24 /// <summary>
25 /// URI origins for which server is expected to be listening.
26 /// </summary>
27 private readonly List<Uri> ownedOriginUris = new List<Uri>();
28
29 /// <summary>
30 /// Origin Uris that are allowed to access data served by this listener.
31 /// </summary>
32 private readonly HashSet<Uri> allowedOriginUris = new HashSet<Uri>();
33
34 /// <summary>
35 /// Mapping between URI paths and factories of request handlers that can correspond
36 /// to them.
37 /// </summary>
38 private readonly Dictionary<string, IHttpRequestHandlerFactory> requestHandlerFactoryMap;
39
40 /// <summary>
41 /// SynchronizationContext wrapper used to track event handlers for Started event.
42 /// </summary>
43 private readonly ContextEventWrapper<EventArgs> startedContextWrapper =
44 new ContextEventWrapper<EventArgs>(ContextSynchronizationMethod.Post);
45
46 /// <summary>
47 /// SynchronizationContext wrapper used to track event handlers for Stopped event.
48 /// </summary>
49 private readonly ContextEventWrapper<EventArgs> stoppedContextWrapper =
50 new ContextEventWrapper<EventArgs>(ContextSynchronizationMethod.Post);
51
52 /// <summary>
53 /// Object used to synchronize access to data shared between client calling thread(s)
54 /// and listener thread.
55 /// </summary>
56 private readonly object lockObject = new object();
57
58 /// <summary>
59 /// Data shared between client calling thread(s) and listener thread.
60 /// </summary>
61 private SharedThreadData threadData;
62
63 /// <summary>
64 /// Initializes a new instance of the ThreadHostedHttpListener class.
65 /// </summary>
66 /// <param name="ownedOrigins">
67 /// URI origins for which server is expected to be listening.
68 /// </param>
69 /// <param name="allowedOrigins">
70 /// Origin Uris that are allowed to access data served by this listener, in addition
71 /// to owned origin Uris. May be empty or null if only owned origins are allowed to
72 /// access server data.
73 /// </param>
74 /// <param name="requestHandlerFactoryMap">
75 /// Mapping between URI paths and factories of request handlers that can correspond
76 /// to them.
77 /// </param>
78 public ThreadHostedHttpListener(IEnumerable<Uri> ownedOrigins, IEnumerable<Uri> allowedOrigins, Dictionary<string, IHttpRequestHandlerFactory> requestHandlerFactoryMap)
79 {
80 if (ownedOrigins == null)
81 {
82 throw new ArgumentNullException("ownedOrigins");
83 }
84
85 if (requestHandlerFactoryMap == null)
86 {
87 throw new ArgumentNullException("requestHandlerFactoryMap");
88 }
89
90 this.requestHandlerFactoryMap = requestHandlerFactoryMap;
91
92 foreach (var origin in ownedOrigins)
93 {
94 this.ownedOriginUris.Add(origin);
95
96 this.allowedOriginUris.Add(origin);
97 }
98
99 if (allowedOrigins != null)
100 {
101 foreach (var origin in allowedOrigins)
102 {
103 this.allowedOriginUris.Add(origin);
104 }
105 }
106 }
107
108 /// <summary>
109 /// Event used to signal that the server has started listening for connections.
110 /// </summary>
111 public event EventHandler<EventArgs> Started
112 {
113 add { this.startedContextWrapper.AddHandler(value); }
114
115 remove { this.startedContextWrapper.RemoveHandler(value); }
116 }
117
118 /// <summary>
119 /// Event used to signal that the server has stopped listening for connections.
120 /// </summary>
121 public event EventHandler<EventArgs> Stopped
122 {
123 add { this.stoppedContextWrapper.AddHandler(value); }
124
125 remove { this.stoppedContextWrapper.RemoveHandler(value); }
126 }
127
128 /// <summary>
129 /// True if listener has a thread actively listening for HTTP requests.
130 /// </summary>
131 public bool IsListening
132 {
133 get
134 {
135 return (this.threadData != null) && this.threadData.Thread.IsAlive;
136 }
137 }
138
139 /// <summary>
140 /// Start listening for requests.
141 /// </summary>
142 public void Start()
143 {
144 lock (this.lockObject)
145 {
146 Thread oldThread = null;
147
148 // If thread is currently running
149 if (this.IsListening)
150 {
151 if (!this.threadData.StopRequestSent)
152 {
153 // Thread is already running and ready to handle requests, so there
154 // is no need to start up a new one.
155 return;
156 }
157
158 // If thread is running, but still in the process of winding down,
159 // dissociate server from currently running thread without waiting for
160 // thread to finish.
161 // New thread will wait for previous thread to finish so that there is
162 // no conflict between two different threads listening on the same URI
163 // prefixes.
164 oldThread = this.threadData.Thread;
165 this.threadData = null;
166 }
167
168 this.threadData = new SharedThreadData
169 {
170 Thread = new Thread(this.ListenerThread),
171 PreviousThread = oldThread
172 };
173 this.threadData.Thread.Start(this.threadData);
174 }
175 }
176
177 /// <summary>
178 /// Stop listening for requests, optionally waiting for thread to finish.
179 /// </summary>
180 /// <param name="wait">
181 /// True if caller wants to wait until listener thread has terminated before
182 /// returning.
183 /// False otherwise.
184 /// </param>
185 public void Stop(bool wait)
186 {
187 Thread listenerThread = null;
188
189 lock (this.lockObject)
190 {
191 if (this.IsListening)
192 {
193 if (!this.threadData.StopRequestSent)
194 {
195 // Request the thread to end, but keep remembering the old thread
196 // data in case listener gets re-started immediately and new thread
197 // has to wait for old thread to finish before getting started.
198 SendStopMessage(this.threadData);
199 }
200
201 if (wait)
202 {
203 listenerThread = this.threadData.Thread;
204 }
205 }
206 }
207
208 if (listenerThread != null)
209 {
210 listenerThread.Join();
211 }
212 }
213
214 /// <summary>
215 /// Stop listening for requests but don't wait for thread to finish.
216 /// </summary>
217 public void Stop()
218 {
219 this.Stop(false);
220 }
221
222 /// <summary>
223 /// Let listener thread know that it should wind down and stop listening for incoming
224 /// HTTP requests.
225 /// </summary>
226 /// <param name="threadData">
227 /// Object containing shared data used to communicate with listener thread.
228 /// </param>
229 private static void SendStopMessage(SharedThreadData threadData)
230 {
231 if (threadData.StateManager != null)
232 {
233 // If there's already a state manager associated with the server thread,
234 // tell it to stop listening.
235 threadData.StateManager.Stop();
236 }
237
238 // Even if there's no dispatcher associated with the server thread yet,
239 // let thread know that it should bail out immediately after starting
240 // up, and never push a dispatcher frame at all.
241 threadData.StopRequestSent = true;
242 }
243
244 /// <summary>
245 /// Thread procedure for HTTP listener thread.
246 /// </summary>
247 /// <param name="data">
248 /// Object containing shared data used to communicate between client thread
249 /// and listener thread.
250 /// </param>
251 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Main listener thread should never crash. Errors are captured in web server trace intead.")]
252 private void ListenerThread(object data)
253 {
254 var sharedData = (SharedThreadData)data;
255
256 lock (this.lockObject)
257 {
258 if (sharedData.StopRequestSent)
259 {
260 return;
261 }
262
263 sharedData.StateManager = new ListenerThreadStateManager(this.ownedOriginUris, this.allowedOriginUris, this.requestHandlerFactoryMap);
264 }
265
266 try
267 {
268 if (sharedData.PreviousThread != null)
269 {
270 // Wait for the previous thread to finish so that only one thread can be listening
271 // on the specified prefixes at any one time.
272 sharedData.PreviousThread.Join();
273 sharedData.PreviousThread = null;
274 }
275
276 //// After this point, it is expected that the only mutation of shared data triggered by
277 //// another thread will be to signal that this thread should stop listening.
278
279 this.startedContextWrapper.Invoke(this, EventArgs.Empty);
280
281 sharedData.StateManager.Listen();
282 }
283 catch (Exception e)
284 {
285 Trace.TraceError("Exception encountered while listening for connections:\n{0}", e);
286 }
287 finally
288 {
289 sharedData.StateManager.Dispose();
290
291 this.stoppedContextWrapper.Invoke(this, EventArgs.Empty);
292 }
293 }
294
295 /// <summary>
296 /// Represents state that needs to be shared between listener thread and client calling thread.
297 /// </summary>
298 private sealed class SharedThreadData
299 {
300 /// <summary>
301 /// Listener thread.
302 /// </summary>
303 public Thread Thread { get; set; }
304
305 /// <summary>
306 /// Object used to manage state and safely communicate with listener thread.
307 /// </summary>
308 public ListenerThreadStateManager StateManager { get; set; }
309
310 /// <summary>
311 /// True if the specific listener thread associated with this shared data
312 /// has previously been requested to stop.
313 /// False otherwise.
314 /// </summary>
315 public bool StopRequestSent { get; set; }
316
317 /// <summary>
318 /// Previous listener thread, which had not fully finished processing before
319 /// a new thread was requested. Most of the time this will be null.
320 /// </summary>
321 public Thread PreviousThread { get; set; }
322 }
323
324 /// <summary>
325 /// Manages state used by a single listener thread.
326 /// </summary>
327 private sealed class ListenerThreadStateManager : IDisposable
328 {
329 /// <summary>
330 /// URI origins for which server is expected to be listening.
331 /// </summary>
332 private readonly List<Uri> ownedOriginUris;
333
334 /// <summary>
335 /// Origin Uris that are allowed to access data served by this listener.
336 /// </summary>
337 private readonly HashSet<Uri> allowedOriginUris;
338
339 /// <summary>
340 /// HttpListener used to wait for incoming HTTP requests.
341 /// </summary>
342 private readonly HttpListener listener = new HttpListener();
343
344 /// <summary>
345 /// Mapping between URI paths and factories of request handlers that can correspond
346 /// to them.
347 /// </summary>
348 private readonly Dictionary<string, IHttpRequestHandlerFactory> requestHandlerFactoryMap = new Dictionary<string, IHttpRequestHandlerFactory>();
349
350 /// <summary>
351 /// Mapping between URI paths and corresponding request handlers.
352 /// </summary>
353 private readonly Dictionary<string, IHttpRequestHandler> requestHandlerMap = new Dictionary<string, IHttpRequestHandler>();
354
355 /// <summary>
356 /// Dispatcher used to manage the queue of work done in listener thread.
357 /// </summary>
358 private readonly Dispatcher dispatcher;
359
360 /// <summary>
361 /// Represents main execution loop in listener thread.
362 /// </summary>
363 private readonly DispatcherFrame frame;
364
365 /// <summary>
366 /// Asynchronous result indicating that we're currently waiting for some HTTP
367 /// client to initiate a request.
368 /// </summary>
369 private IAsyncResult getContextResult;
370
371 /// <summary>
372 /// Initializes a new instance of the ListenerThreadStateManager class.
373 /// </summary>
374 /// <param name="ownedOriginUris">
375 /// URI origins for which server is expected to be listening.
376 /// </param>
377 /// <param name="allowedOriginUris">
378 /// URI origins that are allowed to access data served by this listener.
379 /// </param>
380 /// <param name="requestHandlerFactoryMap">
381 /// Mapping between URI paths and factories of request handlers that can correspond
382 /// to them.
383 /// </param>
384 internal ListenerThreadStateManager(List<Uri> ownedOriginUris, HashSet<Uri> allowedOriginUris, Dictionary<string, IHttpRequestHandlerFactory> requestHandlerFactoryMap)
385 {
386 this.ownedOriginUris = ownedOriginUris;
387 this.allowedOriginUris = allowedOriginUris;
388 this.dispatcher = Dispatcher.CurrentDispatcher;
389 this.frame = new DispatcherFrame { Continue = true };
390 this.requestHandlerFactoryMap = requestHandlerFactoryMap;
391 }
392
393 /// <summary>
394 /// Releases resources used while listening for HTTP requests.
395 /// </summary>
396 public void Dispose()
397 {
398 this.listener.Close();
399 }
400
401 internal void Stop()
402 {
403 this.dispatcher.BeginInvoke((Action)(() =>
404 {
405 foreach (var handler in this.requestHandlerMap)
406 {
407 handler.Value.Cancel();
408 }
409
410 this.frame.Continue = false;
411 }));
412 }
413
414 /// <summary>
415 /// Initializes request handlers, listens for incoming HTTP requests until client
416 /// requests us to stop and then uninitializes request handlers.
417 /// </summary>
418 internal void Listen()
419 {
420 foreach (var entry in this.requestHandlerFactoryMap)
421 {
422 var path = entry.Key;
423
424 // To simplify lookup against "PathAndQuery" property of Uri objects,
425 // we ensure that this has the starting forward slash that PathAndQuery
426 // property values have.
427 if (!path.StartsWith("/", StringComparison.OrdinalIgnoreCase))
428 {
429 path = "/" + path;
430 }
431
432 // Listen for each handler path under each origin
433 foreach (var originUri in this.ownedOriginUris)
434 {
435 // HttpListener only listens to URIs that end in "/", but also remember
436 // path exactly as requested by client associated with request handler,
437 // to match subpath expressions expected by handler
438 var uriBuilder = new UriBuilder(originUri) { Path = path };
439 var prefix = uriBuilder.ToString();
440 if (!prefix.EndsWith("/", StringComparison.OrdinalIgnoreCase))
441 {
442 prefix = prefix + "/";
443 }
444
445 listener.Prefixes.Add(prefix);
446 }
447
448 requestHandlerMap.Add(path, entry.Value.CreateHandler());
449 }
450
451 this.listener.Start();
452 try
453 {
454 var initialize = (Action)(async () =>
455 {
456 foreach (var handler in requestHandlerMap.Values)
457 {
458 await handler.InitializeAsync();
459 }
460
461 this.getContextResult = this.listener.BeginGetContext(this.GetContextCallback, null);
462 });
463 this.dispatcher.BeginInvoke(DispatcherPriority.Normal, initialize);
464 Dispatcher.PushFrame(this.frame);
465
466 var uninitializeFrame = new DispatcherFrame { Continue = true };
467 var uninitialize = (Action)(async () =>
468 {
469 foreach (var handler in this.requestHandlerMap.Values)
470 {
471 await handler.UninitializeAsync();
472 }
473
474 uninitializeFrame.Continue = false;
475 });
476 this.dispatcher.BeginInvoke(DispatcherPriority.Normal, uninitialize);
477 Dispatcher.PushFrame(uninitializeFrame);
478 }
479 finally
480 {
481 this.listener.Stop();
482 }
483 }
484
485 /// <summary>
486 /// Close response stream and associate a status code with response.
487 /// </summary>
488 /// <param name="context">
489 /// Context whose response we should close.
490 /// </param>
491 /// <param name="statusCode">
492 /// Status code.
493 /// </param>
494 private static void CloseResponse(HttpListenerContext context, HttpStatusCode statusCode)
495 {
496 try
497 {
498 context.Response.StatusCode = (int)statusCode;
499 context.Response.Close();
500 }
501 catch (HttpListenerException e)
502 {
503 Trace.TraceWarning(
504 "Problem encountered while sending response for kinect sensor request. Client might have aborted request. Cause: \"{0}\"", e.Message);
505 }
506 }
507
508 /// <summary>
509 /// Checks if this is corresponds to a cross-origin request and, if so, prepares
510 /// the response with the appropriate headers or even body, if necessary.
511 /// </summary>
512 /// <param name="context">
513 /// Listener context containing the request and associated response object.
514 /// </param>
515 /// <returns>
516 /// True if request was initiated by an explicitly allowed origin URI.
517 /// False if origin URI for request is not allowed.
518 /// </returns>
519 private bool HandleCrossOrigin(HttpListenerContext context)
520 {
521 const string OriginHeader = "Origin";
522 const string AllowOriginHeader = "Access-Control-Allow-Origin";
523 const string RequestHeadersHeader = "Access-Control-Request-Headers";
524 const string AllowHeadersHeader = "Access-Control-Allow-Headers";
525 const string RequestMethodHeader = "Access-Control-Request-Method";
526 const string AllowMethodHeader = "Access-Control-Allow-Methods";
527
528 // Origin header is not required, since it is up to browser to
529 // detect when cross-origin security checks are needed.
530 var originValue = context.Request.Headers[OriginHeader];
531 if (originValue != null)
532 {
533 // If origin header is present, check if it's in allowed list
534 Uri originUri;
535 try
536 {
537 originUri = new Uri(originValue);
538 }
539 catch (UriFormatException)
540 {
541 return false;
542 }
543
544 if (!this.allowedOriginUris.Contains(originUri))
545 {
546 return false;
547 }
548
549 // We allow all origins to access this server's data
550 context.Response.Headers.Add(AllowOriginHeader, originValue);
551 }
552
553 var requestHeaders = context.Request.Headers[RequestHeadersHeader];
554 if (requestHeaders != null)
555 {
556 // We allow all headers in cross-origin server requests
557 context.Response.Headers.Add(AllowHeadersHeader, requestHeaders);
558 }
559
560 var requestMethod = context.Request.Headers[RequestMethodHeader];
561 if (requestMethod != null)
562 {
563 // We allow all methods in cross-origin server requests
564 context.Response.Headers.Add(AllowMethodHeader, requestMethod);
565 }
566
567 return true;
568 }
569
570 /// <summary>
571 /// Callback used by listener to let us know when we have received an HTTP
572 /// context corresponding to an earlier call to BeginGetContext.
573 /// </summary>
574 /// <param name="result">
575 /// Status of asynchronous operation.
576 /// </param>
577 private void GetContextCallback(IAsyncResult result)
578 {
579 this.dispatcher.BeginInvoke((Action)(() =>
580 {
581 if (!this.listener.IsListening)
582 {
583 return;
584 }
585
586 Debug.Assert(result == this.getContextResult, "remembered GetContext result should match result handed to callback.");
587
588 var httpListenerContext = this.listener.EndGetContext(result);
589 this.getContextResult = null;
590
591 this.HandleRequestAsync(httpListenerContext);
592
593 if (this.frame.Continue)
594 {
595 this.getContextResult = this.listener.BeginGetContext(this.GetContextCallback, null);
596 }
597 }));
598 }
599
600 /// <summary>
601 /// Handle an HTTP request asynchronously
602 /// </summary>
603 /// <param name="httpListenerContext">
604 /// Context containing HTTP request and response information
605 /// </param>
606 private async void HandleRequestAsync(HttpListenerContext httpListenerContext)
607 {
608 var uri = httpListenerContext.Request.Url;
609 var clientAddress = httpListenerContext.Request.RemoteEndPoint != null
610 ? httpListenerContext.Request.RemoteEndPoint.Address
611 : IPAddress.None;
612 var requestOverview = string.Format("URI=\"{0}\", client=\"{1}\"", uri, clientAddress);
613
614 try
615 {
616 bool foundHandler = false;
617
618 if (!this.HandleCrossOrigin(httpListenerContext))
619 {
620 CloseResponse(httpListenerContext, HttpStatusCode.Forbidden);
621 }
622 else
623 {
624 foreach (var entry in this.requestHandlerMap)
625 {
626 if (uri.PathAndQuery.StartsWith(entry.Key, StringComparison.InvariantCultureIgnoreCase))
627 {
628 foundHandler = true;
629 var subPath = uri.PathAndQuery.Substring(entry.Key.Length);
630 await entry.Value.HandleRequestAsync(httpListenerContext, subPath);
631
632 break;
633 }
634 }
635
636 if (!foundHandler)
637 {
638 CloseResponse(httpListenerContext, HttpStatusCode.NotFound);
639 }
640 }
641
642 Trace.TraceInformation("Request for {0} completed with result: {1}", requestOverview, httpListenerContext.Response.StatusCode);
643 }
644 catch (Exception e)
645 {
646 Trace.TraceError("Exception encountered while handling request for {0}:\n{1}", requestOverview, e);
647 CloseResponse(httpListenerContext, HttpStatusCode.InternalServerError);
648 }
649 }
650 }
651 }
652}
Note: See TracBrowser for help on using the repository browser.