aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Common.Implementations/HttpServer
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Common.Implementations/HttpServer')
-rw-r--r--MediaBrowser.Common.Implementations/HttpServer/BaseRestService.cs452
-rw-r--r--MediaBrowser.Common.Implementations/HttpServer/HttpServer.cs551
-rw-r--r--MediaBrowser.Common.Implementations/HttpServer/NativeWebSocket.cs165
-rw-r--r--MediaBrowser.Common.Implementations/HttpServer/ServerFactory.cs27
4 files changed, 1195 insertions, 0 deletions
diff --git a/MediaBrowser.Common.Implementations/HttpServer/BaseRestService.cs b/MediaBrowser.Common.Implementations/HttpServer/BaseRestService.cs
new file mode 100644
index 000000000..bcf1dc4a7
--- /dev/null
+++ b/MediaBrowser.Common.Implementations/HttpServer/BaseRestService.cs
@@ -0,0 +1,452 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Logging;
+using ServiceStack.Common;
+using ServiceStack.Common.Web;
+using ServiceStack.ServiceHost;
+using ServiceStack.ServiceInterface;
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using MimeTypes = MediaBrowser.Common.Net.MimeTypes;
+using StreamWriter = MediaBrowser.Common.Net.StreamWriter;
+
+namespace MediaBrowser.Common.Implementations.HttpServer
+{
+ /// <summary>
+ /// Class BaseRestService
+ /// </summary>
+ public class BaseRestService : Service, IRestfulService
+ {
+ /// <summary>
+ /// Gets or sets the kernel.
+ /// </summary>
+ /// <value>The kernel.</value>
+ public IKernel Kernel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ public ILogger Logger { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is range request.
+ /// </summary>
+ /// <value><c>true</c> if this instance is range request; otherwise, <c>false</c>.</value>
+ protected bool IsRangeRequest
+ {
+ get
+ {
+ return Request.Headers.AllKeys.Contains("Range");
+ }
+ }
+
+ /// <summary>
+ /// To the optimized result.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="result">The result.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">result</exception>
+ protected object ToOptimizedResult<T>(T result)
+ where T : class
+ {
+ if (result == null)
+ {
+ throw new ArgumentNullException("result");
+ }
+
+ Response.AddHeader("Vary", "Accept-Encoding");
+
+ return RequestContext.ToOptimizedResult(result);
+ }
+
+ /// <summary>
+ /// To the optimized result using cache.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="cacheKey">The cache key.</param>
+ /// <param name="lastDateModified">The last date modified.</param>
+ /// <param name="cacheDuration">Duration of the cache.</param>
+ /// <param name="factoryFn">The factory fn.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">cacheKey</exception>
+ protected object ToOptimizedResultUsingCache<T>(Guid cacheKey, DateTime lastDateModified, TimeSpan? cacheDuration, Func<T> factoryFn)
+ where T : class
+ {
+ if (cacheKey == Guid.Empty)
+ {
+ throw new ArgumentNullException("cacheKey");
+ }
+ if (factoryFn == null)
+ {
+ throw new ArgumentNullException("factoryFn");
+ }
+
+ var key = cacheKey.ToString("N");
+
+ var result = PreProcessCachedResult(cacheKey, key, lastDateModified, cacheDuration, string.Empty);
+
+ if (result != null)
+ {
+ return result;
+ }
+
+ return ToOptimizedResult(factoryFn());
+ }
+
+ /// <summary>
+ /// To the cached result.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="cacheKey">The cache key.</param>
+ /// <param name="lastDateModified">The last date modified.</param>
+ /// <param name="cacheDuration">Duration of the cache.</param>
+ /// <param name="factoryFn">The factory fn.</param>
+ /// <param name="contentType">Type of the content.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">cacheKey</exception>
+ protected object ToCachedResult<T>(Guid cacheKey, DateTime lastDateModified, TimeSpan? cacheDuration, Func<T> factoryFn, string contentType)
+ where T : class
+ {
+ if (cacheKey == Guid.Empty)
+ {
+ throw new ArgumentNullException("cacheKey");
+ }
+ if (factoryFn == null)
+ {
+ throw new ArgumentNullException("factoryFn");
+ }
+
+ var key = cacheKey.ToString("N");
+
+ var result = PreProcessCachedResult(cacheKey, key, lastDateModified, cacheDuration, contentType);
+
+ if (result != null)
+ {
+ return result;
+ }
+
+ return factoryFn();
+ }
+
+ /// <summary>
+ /// To the static file result.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">path</exception>
+ protected object ToStaticFileResult(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var dateModified = File.GetLastWriteTimeUtc(path);
+
+ var cacheKey = path + dateModified.Ticks;
+
+ return ToStaticResult(cacheKey.GetMD5(), dateModified, null, MimeTypes.GetMimeType(path), () => Task.FromResult(GetFileStream(path)));
+ }
+
+ /// <summary>
+ /// Gets the file stream.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>Stream.</returns>
+ private Stream GetFileStream(string path)
+ {
+ return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous);
+ }
+
+ /// <summary>
+ /// To the static result.
+ /// </summary>
+ /// <param name="cacheKey">The cache key.</param>
+ /// <param name="lastDateModified">The last date modified.</param>
+ /// <param name="cacheDuration">Duration of the cache.</param>
+ /// <param name="contentType">Type of the content.</param>
+ /// <param name="factoryFn">The factory fn.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">cacheKey</exception>
+ protected object ToStaticResult(Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType, Func<Task<Stream>> factoryFn)
+ {
+ if (cacheKey == Guid.Empty)
+ {
+ throw new ArgumentNullException("cacheKey");
+ }
+ if (factoryFn == null)
+ {
+ throw new ArgumentNullException("factoryFn");
+ }
+
+ var key = cacheKey.ToString("N");
+
+ var result = PreProcessCachedResult(cacheKey, key, lastDateModified, cacheDuration, contentType);
+
+ if (result != null)
+ {
+ return result;
+ }
+
+ var compress = ShouldCompressResponse(contentType);
+
+ if (compress)
+ {
+ Response.AddHeader("Vary", "Accept-Encoding");
+ }
+
+ return ToStaticResult(contentType, factoryFn, compress).Result;
+ }
+
+ /// <summary>
+ /// Shoulds the compress response.
+ /// </summary>
+ /// <param name="contentType">Type of the content.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool ShouldCompressResponse(string contentType)
+ {
+ // It will take some work to support compression with byte range requests
+ if (IsRangeRequest)
+ {
+ return false;
+ }
+
+ // Don't compress media
+ if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Don't compress images
+ if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ if (contentType.StartsWith("application/", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// To the static result.
+ /// </summary>
+ /// <param name="contentType">Type of the content.</param>
+ /// <param name="factoryFn">The factory fn.</param>
+ /// <param name="compress">if set to <c>true</c> [compress].</param>
+ /// <returns>System.Object.</returns>
+ private async Task<object> ToStaticResult(string contentType, Func<Task<Stream>> factoryFn, bool compress)
+ {
+ if (!compress || string.IsNullOrEmpty(RequestContext.CompressionType))
+ {
+ Response.ContentType = contentType;
+
+ var stream = await factoryFn().ConfigureAwait(false);
+
+ return new StreamWriter(stream);
+ }
+
+ string content;
+
+ using (var stream = await factoryFn().ConfigureAwait(false))
+ {
+ using (var reader = new StreamReader(stream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var contents = content.Compress(RequestContext.CompressionType);
+
+ return new CompressedResult(contents, RequestContext.CompressionType, contentType);
+ }
+
+ /// <summary>
+ /// Pres the process optimized result.
+ /// </summary>
+ /// <param name="cacheKey">The cache key.</param>
+ /// <param name="cacheKeyString">The cache key string.</param>
+ /// <param name="lastDateModified">The last date modified.</param>
+ /// <param name="cacheDuration">Duration of the cache.</param>
+ /// <param name="contentType">Type of the content.</param>
+ /// <returns>System.Object.</returns>
+ private object PreProcessCachedResult(Guid cacheKey, string cacheKeyString, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType)
+ {
+ Response.AddHeader("ETag", cacheKeyString);
+
+ if (IsNotModified(cacheKey, lastDateModified, cacheDuration))
+ {
+ AddAgeHeader(lastDateModified);
+ AddExpiresHeader(cacheKeyString, cacheDuration);
+ //ctx.Response.SendChunked = false;
+
+ if (!string.IsNullOrEmpty(contentType))
+ {
+ Response.ContentType = contentType;
+ }
+
+ return new HttpResult(new byte[] { }, HttpStatusCode.NotModified);
+ }
+
+ SetCachingHeaders(cacheKeyString, lastDateModified, cacheDuration);
+
+ return null;
+ }
+
+ /// <summary>
+ /// Determines whether [is not modified] [the specified cache key].
+ /// </summary>
+ /// <param name="cacheKey">The cache key.</param>
+ /// <param name="lastDateModified">The last date modified.</param>
+ /// <param name="cacheDuration">Duration of the cache.</param>
+ /// <returns><c>true</c> if [is not modified] [the specified cache key]; otherwise, <c>false</c>.</returns>
+ private bool IsNotModified(Guid? cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration)
+ {
+ var isNotModified = true;
+
+ if (Request.Headers.AllKeys.Contains("If-Modified-Since"))
+ {
+ DateTime ifModifiedSince;
+
+ if (DateTime.TryParse(Request.Headers["If-Modified-Since"], out ifModifiedSince))
+ {
+ isNotModified = IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified);
+ }
+ }
+
+ // Validate If-None-Match
+ if (isNotModified && (cacheKey.HasValue || !string.IsNullOrEmpty(Request.Headers["If-None-Match"])))
+ {
+ Guid ifNoneMatch;
+
+ if (Guid.TryParse(Request.Headers["If-None-Match"] ?? string.Empty, out ifNoneMatch))
+ {
+ if (cacheKey.HasValue && cacheKey.Value == ifNoneMatch)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determines whether [is not modified] [the specified if modified since].
+ /// </summary>
+ /// <param name="ifModifiedSince">If modified since.</param>
+ /// <param name="cacheDuration">Duration of the cache.</param>
+ /// <param name="dateModified">The date modified.</param>
+ /// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns>
+ private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified)
+ {
+ if (dateModified.HasValue)
+ {
+ var lastModified = NormalizeDateForComparison(dateModified.Value);
+ ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
+
+ return lastModified <= ifModifiedSince;
+ }
+
+ if (cacheDuration.HasValue)
+ {
+ var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value);
+
+ if (DateTime.UtcNow < cacheExpirationDate)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ /// <summary>
+ /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
+ /// </summary>
+ /// <param name="date">The date.</param>
+ /// <returns>DateTime.</returns>
+ private DateTime NormalizeDateForComparison(DateTime date)
+ {
+ return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
+ }
+
+ /// <summary>
+ /// Sets the caching headers.
+ /// </summary>
+ /// <param name="cacheKey">The cache key.</param>
+ /// <param name="lastDateModified">The last date modified.</param>
+ /// <param name="cacheDuration">Duration of the cache.</param>
+ private void SetCachingHeaders(string cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration)
+ {
+ // Don't specify both last modified and Etag, unless caching unconditionally. They are redundant
+ // https://developers.google.com/speed/docs/best-practices/caching#LeverageBrowserCaching
+ if (lastDateModified.HasValue && (string.IsNullOrEmpty(cacheKey) || cacheDuration.HasValue))
+ {
+ AddAgeHeader(lastDateModified);
+ Response.AddHeader("LastModified", lastDateModified.Value.ToString("r"));
+ }
+
+ if (cacheDuration.HasValue)
+ {
+ Response.AddHeader("Cache-Control", "public, max-age=" + Convert.ToInt32(cacheDuration.Value.TotalSeconds));
+ }
+ else if (!string.IsNullOrEmpty(cacheKey))
+ {
+ Response.AddHeader("Cache-Control", "public");
+ }
+ else
+ {
+ Response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ Response.AddHeader("pragma", "no-cache, no-store, must-revalidate");
+ }
+
+ AddExpiresHeader(cacheKey, cacheDuration);
+ }
+
+ /// <summary>
+ /// Adds the expires header.
+ /// </summary>
+ /// <param name="cacheKey">The cache key.</param>
+ /// <param name="cacheDuration">Duration of the cache.</param>
+ private void AddExpiresHeader(string cacheKey, TimeSpan? cacheDuration)
+ {
+ if (cacheDuration.HasValue)
+ {
+ Response.AddHeader("Expires", DateTime.UtcNow.Add(cacheDuration.Value).ToString("r"));
+ }
+ else if (string.IsNullOrEmpty(cacheKey))
+ {
+ Response.AddHeader("Expires", "-1");
+ }
+ }
+
+ /// <summary>
+ /// Adds the age header.
+ /// </summary>
+ /// <param name="lastDateModified">The last date modified.</param>
+ private void AddAgeHeader(DateTime? lastDateModified)
+ {
+ if (lastDateModified.HasValue)
+ {
+ Response.AddHeader("Age", Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture));
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common.Implementations/HttpServer/HttpServer.cs b/MediaBrowser.Common.Implementations/HttpServer/HttpServer.cs
new file mode 100644
index 000000000..57ba6163b
--- /dev/null
+++ b/MediaBrowser.Common.Implementations/HttpServer/HttpServer.cs
@@ -0,0 +1,551 @@
+using Funq;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using ServiceStack.Api.Swagger;
+using ServiceStack.Common.Web;
+using ServiceStack.Configuration;
+using ServiceStack.Logging.NLogger;
+using ServiceStack.ServiceHost;
+using ServiceStack.ServiceInterface.Cors;
+using ServiceStack.Text;
+using ServiceStack.WebHost.Endpoints;
+using ServiceStack.WebHost.Endpoints.Extensions;
+using ServiceStack.WebHost.Endpoints.Support;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.WebSockets;
+using System.Reactive.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Implementations.HttpServer
+{
+ /// <summary>
+ /// Class HttpServer
+ /// </summary>
+ public class HttpServer : HttpListenerBase, IHttpServer
+ {
+ /// <summary>
+ /// The logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Gets the URL prefix.
+ /// </summary>
+ /// <value>The URL prefix.</value>
+ public string UrlPrefix { get; private set; }
+
+ /// <summary>
+ /// The _rest services
+ /// </summary>
+ private readonly List<IRestfulService> _restServices = new List<IRestfulService>();
+
+ /// <summary>
+ /// Gets or sets the application host.
+ /// </summary>
+ /// <value>The application host.</value>
+ private IApplicationHost ApplicationHost { get; set; }
+
+ /// <summary>
+ /// This subscribes to HttpListener requests and finds the appropriate BaseHandler to process it
+ /// </summary>
+ /// <value>The HTTP listener.</value>
+ private IDisposable HttpListener { get; set; }
+
+ /// <summary>
+ /// Gets or sets the protobuf serializer.
+ /// </summary>
+ /// <value>The protobuf serializer.</value>
+ private IProtobufSerializer ProtobufSerializer { get; set; }
+
+ /// <summary>
+ /// Occurs when [web socket connected].
+ /// </summary>
+ public event EventHandler<WebSocketConnectEventArgs> WebSocketConnected;
+
+ /// <summary>
+ /// Gets the default redirect path.
+ /// </summary>
+ /// <value>The default redirect path.</value>
+ private string DefaultRedirectPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the server.
+ /// </summary>
+ /// <value>The name of the server.</value>
+ private string ServerName { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpServer" /> class.
+ /// </summary>
+ /// <param name="applicationHost">The application host.</param>
+ /// <param name="protobufSerializer">The protobuf serializer.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="serverName">Name of the server.</param>
+ /// <param name="defaultRedirectpath">The default redirectpath.</param>
+ /// <exception cref="System.ArgumentNullException">urlPrefix</exception>
+ public HttpServer(IApplicationHost applicationHost, IProtobufSerializer protobufSerializer, ILogger logger, string serverName, string defaultRedirectpath)
+ : base()
+ {
+ if (protobufSerializer == null)
+ {
+ throw new ArgumentNullException("protobufSerializer");
+ }
+ if (logger == null)
+ {
+ throw new ArgumentNullException("logger");
+ }
+ if (applicationHost == null)
+ {
+ throw new ArgumentNullException("applicationHost");
+ }
+ if (string.IsNullOrEmpty(serverName))
+ {
+ throw new ArgumentNullException("serverName");
+ }
+ if (string.IsNullOrEmpty(defaultRedirectpath))
+ {
+ throw new ArgumentNullException("defaultRedirectpath");
+ }
+
+ ServerName = serverName;
+ DefaultRedirectPath = defaultRedirectpath;
+ ProtobufSerializer = protobufSerializer;
+ _logger = logger;
+ ApplicationHost = applicationHost;
+
+ EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath = null;
+ EndpointHostConfig.Instance.MetadataRedirectPath = "metadata";
+ }
+
+ /// <summary>
+ /// Configures the specified container.
+ /// </summary>
+ /// <param name="container">The container.</param>
+ public override void Configure(Container container)
+ {
+ JsConfig.DateHandler = JsonDateHandler.ISO8601;
+ JsConfig.ExcludeTypeInfo = true;
+ JsConfig.IncludeNullValues = false;
+
+ SetConfig(new EndpointHostConfig
+ {
+ DefaultRedirectPath = DefaultRedirectPath,
+
+ // Tell SS to bubble exceptions up to here
+ WriteErrorsToResponse = false,
+
+ DebugMode = true
+ });
+
+ container.Adapter = new ContainerAdapter(ApplicationHost);
+
+ Plugins.Add(new SwaggerFeature());
+ Plugins.Add(new CorsFeature());
+
+ ServiceStack.Logging.LogManager.LogFactory = new NLogFactory();
+ }
+
+ /// <summary>
+ /// Starts the Web Service
+ /// </summary>
+ /// <param name="urlBase">A Uri that acts as the base that the server is listening on.
+ /// Format should be: http://127.0.0.1:8080/ or http://127.0.0.1:8080/somevirtual/
+ /// Note: the trailing slash is required! For more info see the
+ /// HttpListener.Prefixes property on MSDN.</param>
+ public override void Start(string urlBase)
+ {
+ if (string.IsNullOrEmpty(urlBase))
+ {
+ throw new ArgumentNullException("urlBase");
+ }
+
+ // *** Already running - just leave it in place
+ if (IsStarted)
+ {
+ return;
+ }
+
+ if (Listener == null)
+ {
+ Listener = new HttpListener();
+ }
+
+ EndpointHost.Config.ServiceStackHandlerFactoryPath = HttpListenerRequestWrapper.GetHandlerPathIfAny(urlBase);
+
+ UrlPrefix = urlBase;
+
+ Listener.Prefixes.Add(urlBase);
+
+ IsStarted = true;
+ Listener.Start();
+
+ HttpListener = CreateObservableStream().Subscribe(ProcessHttpRequestAsync);
+ }
+
+ /// <summary>
+ /// Creates the observable stream.
+ /// </summary>
+ /// <returns>IObservable{HttpListenerContext}.</returns>
+ private IObservable<HttpListenerContext> CreateObservableStream()
+ {
+ return Observable.Create<HttpListenerContext>(obs =>
+ Observable.FromAsync(() => Listener.GetContextAsync())
+ .Subscribe(obs))
+ .Repeat()
+ .Retry()
+ .Publish()
+ .RefCount();
+ }
+
+ /// <summary>
+ /// Processes incoming http requests by routing them to the appropiate handler
+ /// </summary>
+ /// <param name="context">The CTX.</param>
+ private async void ProcessHttpRequestAsync(HttpListenerContext context)
+ {
+ LogHttpRequest(context);
+
+ if (context.Request.IsWebSocketRequest)
+ {
+ await ProcessWebSocketRequest(context).ConfigureAwait(false);
+ return;
+ }
+
+
+ Task.Run(() =>
+ {
+ RaiseReceiveWebRequest(context);
+
+ try
+ {
+ ProcessRequest(context);
+ }
+ catch (InvalidOperationException ex)
+ {
+ HandleException(context.Response, ex, 422);
+
+ throw;
+ }
+ catch (ResourceNotFoundException ex)
+ {
+ HandleException(context.Response, ex, 404);
+
+ throw;
+ }
+ catch (FileNotFoundException ex)
+ {
+ HandleException(context.Response, ex, 404);
+
+ throw;
+ }
+ catch (DirectoryNotFoundException ex)
+ {
+ HandleException(context.Response, ex, 404);
+
+ throw;
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ HandleException(context.Response, ex, 401);
+
+ throw;
+ }
+ catch (ArgumentException ex)
+ {
+ HandleException(context.Response, ex, 400);
+
+ throw;
+ }
+ catch (Exception ex)
+ {
+ HandleException(context.Response, ex, 500);
+
+ throw;
+ }
+ });
+ }
+
+ /// <summary>
+ /// Processes the web socket request.
+ /// </summary>
+ /// <param name="ctx">The CTX.</param>
+ /// <returns>Task.</returns>
+ private async Task ProcessWebSocketRequest(HttpListenerContext ctx)
+ {
+ try
+ {
+ var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false);
+
+ if (WebSocketConnected != null)
+ {
+ WebSocketConnected(this, new WebSocketConnectEventArgs { WebSocket = new NativeWebSocket(webSocketContext.WebSocket, _logger), Endpoint = ctx.Request.RemoteEndPoint.ToString() });
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("AcceptWebSocketAsync error", ex);
+
+ ctx.Response.StatusCode = 500;
+ ctx.Response.Close();
+ }
+ }
+
+ /// <summary>
+ /// Logs the HTTP request.
+ /// </summary>
+ /// <param name="ctx">The CTX.</param>
+ private void LogHttpRequest(HttpListenerContext ctx)
+ {
+ var log = new StringBuilder();
+
+ log.AppendLine("Url: " + ctx.Request.Url);
+ log.AppendLine("Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k])));
+
+ var type = ctx.Request.IsWebSocketRequest ? "Web Socket" : "HTTP " + ctx.Request.HttpMethod;
+
+ if (EnableHttpRequestLogging)
+ {
+ _logger.LogMultiline(type + " request received from " + ctx.Request.RemoteEndPoint, LogSeverity.Debug, log);
+ }
+ }
+
+ /// <summary>
+ /// Appends the error message.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ /// <param name="ex">The ex.</param>
+ /// <param name="statusCode">The status code.</param>
+ private void HandleException(HttpListenerResponse response, Exception ex, int statusCode)
+ {
+ _logger.ErrorException("Error processing request", ex);
+
+ response.StatusCode = statusCode;
+
+ response.Headers.Add("Status", statusCode.ToString(new CultureInfo("en-US")));
+
+ response.Headers.Remove("Age");
+ response.Headers.Remove("Expires");
+ response.Headers.Remove("Cache-Control");
+ response.Headers.Remove("Etag");
+ response.Headers.Remove("Last-Modified");
+
+ response.ContentType = "text/plain";
+
+ if (!string.IsNullOrEmpty(ex.Message))
+ {
+ response.AddHeader("X-Application-Error-Code", ex.Message);
+ }
+
+ // This could fail, but try to add the stack trace as the body content
+ try
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("{");
+ sb.AppendLine("\"ResponseStatus\":{");
+ sb.AppendFormat(" \"ErrorCode\":{0},\n", ex.GetType().Name.EncodeJson());
+ sb.AppendFormat(" \"Message\":{0},\n", ex.Message.EncodeJson());
+ sb.AppendFormat(" \"StackTrace\":{0}\n", ex.StackTrace.EncodeJson());
+ sb.AppendLine("}");
+ sb.AppendLine("}");
+
+ response.StatusCode = 500;
+ response.ContentType = ContentType.Json;
+ var sbBytes = sb.ToString().ToUtf8Bytes();
+ response.OutputStream.Write(sbBytes, 0, sbBytes.Length);
+ response.Close();
+ }
+ catch (Exception errorEx)
+ {
+ _logger.ErrorException("Error processing failed request", errorEx);
+ }
+ }
+
+
+ /// <summary>
+ /// Overridable method that can be used to implement a custom hnandler
+ /// </summary>
+ /// <param name="context">The context.</param>
+ /// <exception cref="System.NotImplementedException">Cannot execute handler: + handler + at PathInfo: + httpReq.PathInfo</exception>
+ protected override void ProcessRequest(HttpListenerContext context)
+ {
+ if (string.IsNullOrEmpty(context.Request.RawUrl)) return;
+
+ var operationName = context.Request.GetOperationName();
+
+ var httpReq = new HttpListenerRequestWrapper(operationName, context.Request);
+ var httpRes = new HttpListenerResponseWrapper(context.Response);
+ var handler = ServiceStackHttpHandlerFactory.GetHandler(httpReq);
+
+ var serviceStackHandler = handler as IServiceStackHttpHandler;
+
+ if (serviceStackHandler != null)
+ {
+ var restHandler = serviceStackHandler as RestHandler;
+ if (restHandler != null)
+ {
+ httpReq.OperationName = operationName = restHandler.RestPath.RequestType.Name;
+ }
+ serviceStackHandler.ProcessRequest(httpReq, httpRes, operationName);
+ LogResponse(context);
+ httpRes.Close();
+ return;
+ }
+
+ throw new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo);
+ }
+
+ /// <summary>
+ /// Logs the response.
+ /// </summary>
+ /// <param name="ctx">The CTX.</param>
+ private void LogResponse(HttpListenerContext ctx)
+ {
+ var statusode = ctx.Response.StatusCode;
+
+ var log = new StringBuilder();
+
+ log.AppendLine(string.Format("Url: {0}", ctx.Request.Url));
+
+ log.AppendLine("Headers: " + string.Join(",", ctx.Response.Headers.AllKeys.Select(k => k + "=" + ctx.Response.Headers[k])));
+
+ var msg = "Http Response Sent (" + statusode + ") to " + ctx.Request.RemoteEndPoint;
+
+ if (EnableHttpRequestLogging)
+ {
+ _logger.LogMultiline(msg, LogSeverity.Debug, log);
+ }
+ }
+
+ /// <summary>
+ /// Creates the service manager.
+ /// </summary>
+ /// <param name="assembliesWithServices">The assemblies with services.</param>
+ /// <returns>ServiceManager.</returns>
+ protected override ServiceManager CreateServiceManager(params Assembly[] assembliesWithServices)
+ {
+ var types = _restServices.Select(r => r.GetType()).ToArray();
+
+ return new ServiceManager(new Container(), new ServiceController(() => types));
+ }
+
+ /// <summary>
+ /// Shut down the Web Service
+ /// </summary>
+ public override void Stop()
+ {
+ if (HttpListener != null)
+ {
+ HttpListener.Dispose();
+ HttpListener = null;
+ }
+
+ if (Listener != null)
+ {
+ Listener.Prefixes.Remove(UrlPrefix);
+ }
+
+ base.Stop();
+ }
+
+ /// <summary>
+ /// The _supports native web socket
+ /// </summary>
+ private bool? _supportsNativeWebSocket;
+
+ /// <summary>
+ /// Gets a value indicating whether [supports web sockets].
+ /// </summary>
+ /// <value><c>true</c> if [supports web sockets]; otherwise, <c>false</c>.</value>
+ public bool SupportsWebSockets
+ {
+ get
+ {
+ if (!_supportsNativeWebSocket.HasValue)
+ {
+ try
+ {
+ new ClientWebSocket();
+
+ _supportsNativeWebSocket = true;
+ }
+ catch (PlatformNotSupportedException)
+ {
+ _supportsNativeWebSocket = false;
+ }
+ }
+
+ return _supportsNativeWebSocket.Value;
+ }
+ }
+
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable HTTP request logging].
+ /// </summary>
+ /// <value><c>true</c> if [enable HTTP request logging]; otherwise, <c>false</c>.</value>
+ public bool EnableHttpRequestLogging { get; set; }
+
+ /// <summary>
+ /// Adds the rest handlers.
+ /// </summary>
+ /// <param name="services">The services.</param>
+ public void Init(IEnumerable<IRestfulService> services)
+ {
+ _restServices.AddRange(services);
+
+ EndpointHost.ConfigureHost(this, ServerName, CreateServiceManager());
+ ContentTypeFilters.Register(ContentType.ProtoBuf, (reqCtx, res, stream) => ProtobufSerializer.SerializeToStream(res, stream), (type, stream) => ProtobufSerializer.DeserializeFromStream(stream, type));
+
+ Init();
+ }
+ }
+
+ /// <summary>
+ /// Class ContainerAdapter
+ /// </summary>
+ class ContainerAdapter : IContainerAdapter
+ {
+ /// <summary>
+ /// The _app host
+ /// </summary>
+ private readonly IApplicationHost _appHost;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ContainerAdapter" /> class.
+ /// </summary>
+ /// <param name="appHost">The app host.</param>
+ public ContainerAdapter(IApplicationHost appHost)
+ {
+ _appHost = appHost;
+ }
+ /// <summary>
+ /// Resolves this instance.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>``0.</returns>
+ public T Resolve<T>()
+ {
+ return _appHost.Resolve<T>();
+ }
+
+ /// <summary>
+ /// Tries the resolve.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>``0.</returns>
+ public T TryResolve<T>()
+ {
+ return _appHost.TryResolve<T>();
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common.Implementations/HttpServer/NativeWebSocket.cs b/MediaBrowser.Common.Implementations/HttpServer/NativeWebSocket.cs
new file mode 100644
index 000000000..97bab96f8
--- /dev/null
+++ b/MediaBrowser.Common.Implementations/HttpServer/NativeWebSocket.cs
@@ -0,0 +1,165 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+using WebSocketMessageType = MediaBrowser.Common.Net.WebSocketMessageType;
+using WebSocketState = MediaBrowser.Common.Net.WebSocketState;
+
+namespace MediaBrowser.Common.Implementations.HttpServer
+{
+ /// <summary>
+ /// Class NativeWebSocket
+ /// </summary>
+ public class NativeWebSocket : IWebSocket
+ {
+ /// <summary>
+ /// The logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Gets or sets the web socket.
+ /// </summary>
+ /// <value>The web socket.</value>
+ private System.Net.WebSockets.WebSocket WebSocket { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NativeWebSocket" /> class.
+ /// </summary>
+ /// <param name="socket">The socket.</param>
+ /// <param name="logger">The logger.</param>
+ /// <exception cref="System.ArgumentNullException">socket</exception>
+ public NativeWebSocket(System.Net.WebSockets.WebSocket socket, ILogger logger)
+ {
+ if (socket == null)
+ {
+ throw new ArgumentNullException("socket");
+ }
+
+ if (logger == null)
+ {
+ throw new ArgumentNullException("logger");
+ }
+
+ _logger = logger;
+ WebSocket = socket;
+
+ Receive();
+ }
+
+ /// <summary>
+ /// Gets or sets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ public WebSocketState State
+ {
+ get
+ {
+ WebSocketState commonState;
+
+ if (!Enum.TryParse(WebSocket.State.ToString(), true, out commonState))
+ {
+ _logger.Warn("Unrecognized WebSocketState: {0}", WebSocket.State.ToString());
+ }
+
+ return commonState;
+ }
+ }
+
+ /// <summary>
+ /// Receives this instance.
+ /// </summary>
+ private async void Receive()
+ {
+ while (true)
+ {
+ byte[] bytes;
+
+ try
+ {
+ bytes = await ReceiveBytesAsync(CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (WebSocketException ex)
+ {
+ _logger.ErrorException("Error reveiving web socket message", ex);
+
+ break;
+ }
+
+ if (OnReceiveDelegate != null)
+ {
+ OnReceiveDelegate(bytes);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Receives the async.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{WebSocketMessageInfo}.</returns>
+ /// <exception cref="System.Net.WebSockets.WebSocketException">Connection closed</exception>
+ private async Task<byte[]> ReceiveBytesAsync(CancellationToken cancellationToken)
+ {
+ var bytes = new byte[4096];
+ var buffer = new ArraySegment<byte>(bytes);
+
+ var result = await WebSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
+
+ if (result.CloseStatus.HasValue)
+ {
+ throw new WebSocketException("Connection closed");
+ }
+
+ return buffer.Array;
+ }
+
+ /// <summary>
+ /// Sends the async.
+ /// </summary>
+ /// <param name="bytes">The bytes.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken)
+ {
+ System.Net.WebSockets.WebSocketMessageType nativeType;
+
+ if (!Enum.TryParse(type.ToString(), true, out nativeType))
+ {
+ _logger.Warn("Unrecognized WebSocketMessageType: {0}", type.ToString());
+ }
+
+ return WebSocket.SendAsync(new ArraySegment<byte>(bytes), nativeType, true, cancellationToken);
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ WebSocket.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the receive action.
+ /// </summary>
+ /// <value>The receive action.</value>
+ public Action<byte[]> OnReceiveDelegate { get; set; }
+ }
+}
diff --git a/MediaBrowser.Common.Implementations/HttpServer/ServerFactory.cs b/MediaBrowser.Common.Implementations/HttpServer/ServerFactory.cs
new file mode 100644
index 000000000..743bd60c4
--- /dev/null
+++ b/MediaBrowser.Common.Implementations/HttpServer/ServerFactory.cs
@@ -0,0 +1,27 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Common.Implementations.HttpServer
+{
+ /// <summary>
+ /// Class ServerFactory
+ /// </summary>
+ public static class ServerFactory
+ {
+ /// <summary>
+ /// Creates the server.
+ /// </summary>
+ /// <param name="applicationHost">The application host.</param>
+ /// <param name="protobufSerializer">The protobuf serializer.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="serverName">Name of the server.</param>
+ /// <param name="defaultRedirectpath">The default redirectpath.</param>
+ /// <returns>IHttpServer.</returns>
+ public static IHttpServer CreateServer(IApplicationHost applicationHost, IProtobufSerializer protobufSerializer, ILogger logger, string serverName, string defaultRedirectpath)
+ {
+ return new HttpServer(applicationHost, protobufSerializer, logger, serverName, defaultRedirectpath);
+ }
+ }
+}