aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Common/Net
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Common/Net')
-rw-r--r--MediaBrowser.Common/Net/AlchemyWebSocket.cs131
-rw-r--r--MediaBrowser.Common/Net/BaseRestService.cs451
-rw-r--r--MediaBrowser.Common/Net/Handlers/BaseActionHandler.cs31
-rw-r--r--MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs23
-rw-r--r--MediaBrowser.Common/Net/Handlers/BaseHandler.cs1254
-rw-r--r--MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs224
-rw-r--r--MediaBrowser.Common/Net/Handlers/IHttpServerHandler.cs32
-rw-r--r--MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs513
-rw-r--r--MediaBrowser.Common/Net/HttpManager.cs452
-rw-r--r--MediaBrowser.Common/Net/HttpServer.cs482
-rw-r--r--MediaBrowser.Common/Net/IRestfulService.cs14
-rw-r--r--MediaBrowser.Common/Net/IWebSocket.cs35
-rw-r--r--MediaBrowser.Common/Net/MimeTypes.cs366
-rw-r--r--MediaBrowser.Common/Net/NativeWebSocket.cs153
-rw-r--r--MediaBrowser.Common/Net/NetUtils.cs219
-rw-r--r--MediaBrowser.Common/Net/NetworkShares.cs644
-rw-r--r--MediaBrowser.Common/Net/Request.cs18
-rw-r--r--MediaBrowser.Common/Net/StaticResult.cs14
-rw-r--r--MediaBrowser.Common/Net/UdpServer.cs142
-rw-r--r--MediaBrowser.Common/Net/WebSocketConnection.cs228
20 files changed, 4418 insertions, 1008 deletions
diff --git a/MediaBrowser.Common/Net/AlchemyWebSocket.cs b/MediaBrowser.Common/Net/AlchemyWebSocket.cs
new file mode 100644
index 0000000000..1971990db9
--- /dev/null
+++ b/MediaBrowser.Common/Net/AlchemyWebSocket.cs
@@ -0,0 +1,131 @@
+using Alchemy.Classes;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Class AlchemyWebSocket
+ /// </summary>
+ public class AlchemyWebSocket : IWebSocket
+ {
+ /// <summary>
+ /// The logger
+ /// </summary>
+ private static ILogger Logger = LogManager.GetLogger("AlchemyWebSocket");
+
+ /// <summary>
+ /// Gets or sets the web socket.
+ /// </summary>
+ /// <value>The web socket.</value>
+ private UserContext UserContext { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AlchemyWebSocket" /> class.
+ /// </summary>
+ /// <param name="context">The context.</param>
+ /// <exception cref="System.ArgumentNullException">context</exception>
+ public AlchemyWebSocket(UserContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+
+ UserContext = context;
+
+ context.SetOnDisconnect(OnDisconnected);
+ context.SetOnReceive(OnReceive);
+
+ Logger.Info("Client connected from {0}", context.ClientAddress);
+ }
+
+ /// <summary>
+ /// The _disconnected
+ /// </summary>
+ private bool _disconnected = false;
+ /// <summary>
+ /// Gets or sets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ public WebSocketState State
+ {
+ get { return _disconnected ? WebSocketState.Closed : WebSocketState.Open; }
+ }
+
+ /// <summary>
+ /// Called when [disconnected].
+ /// </summary>
+ /// <param name="context">The context.</param>
+ private void OnDisconnected(UserContext context)
+ {
+ _disconnected = true;
+ }
+
+ /// <summary>
+ /// Called when [receive].
+ /// </summary>
+ /// <param name="context">The context.</param>
+ private void OnReceive(UserContext context)
+ {
+ if (OnReceiveDelegate != null)
+ {
+ var json = context.DataFrame.ToString();
+
+ if (!string.IsNullOrWhiteSpace(json))
+ {
+ try
+ {
+ var messageResult = JsonSerializer.DeserializeFromString<WebSocketMessageInfo>(json);
+
+ OnReceiveDelegate(messageResult);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error processing web socket message", ex);
+ }
+ }
+ }
+ }
+
+ /// <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)
+ {
+ return Task.Run(() => UserContext.Send(bytes));
+ }
+
+ /// <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)
+ {
+ }
+
+ /// <summary>
+ /// Gets or sets the receive action.
+ /// </summary>
+ /// <value>The receive action.</value>
+ public Action<WebSocketMessageInfo> OnReceiveDelegate { get; set; }
+ }
+}
diff --git a/MediaBrowser.Common/Net/BaseRestService.cs b/MediaBrowser.Common/Net/BaseRestService.cs
new file mode 100644
index 0000000000..cfec9398db
--- /dev/null
+++ b/MediaBrowser.Common/Net/BaseRestService.cs
@@ -0,0 +1,451 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Model.Logging;
+using ServiceStack.Common;
+using ServiceStack.Common.Web;
+using ServiceStack.ServiceHost;
+using ServiceStack.ServiceInterface;
+using ServiceStack.WebHost.Endpoints;
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Class BaseRestService
+ /// </summary>
+ public class BaseRestService : Service, IRestfulService
+ {
+ /// <summary>
+ /// The logger
+ /// </summary>
+ protected ILogger Logger = LogManager.GetLogger("RestService");
+
+ /// <summary>
+ /// Gets or sets the kernel.
+ /// </summary>
+ /// <value>The kernel.</value>
+ public IKernel Kernel { 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>
+ /// Adds the routes.
+ /// </summary>
+ /// <param name="appHost">The app host.</param>
+ public virtual void Configure(IAppHost appHost)
+ {
+ }
+
+ /// <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;
+ }
+
+ 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;
+ return await factoryFn().ConfigureAwait(false);
+ }
+
+ 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/Net/Handlers/BaseActionHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseActionHandler.cs
new file mode 100644
index 0000000000..72df88519b
--- /dev/null
+++ b/MediaBrowser.Common/Net/Handlers/BaseActionHandler.cs
@@ -0,0 +1,31 @@
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Entities;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+ /// <summary>
+ /// Class BaseActionHandler
+ /// </summary>
+ /// <typeparam name="TKernelType">The type of the T kernel type.</typeparam>
+ public abstract class BaseActionHandler<TKernelType> : BaseSerializationHandler<TKernelType, EmptyRequestResult>
+ where TKernelType : IKernel
+ {
+ /// <summary>
+ /// Gets the object to serialize.
+ /// </summary>
+ /// <returns>Task{EmptyRequestResult}.</returns>
+ protected override async Task<EmptyRequestResult> GetObjectToSerialize()
+ {
+ await ExecuteAction();
+
+ return new EmptyRequestResult();
+ }
+
+ /// <summary>
+ /// Performs the action.
+ /// </summary>
+ /// <returns>Task.</returns>
+ protected abstract Task ExecuteAction();
+ }
+}
diff --git a/MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs
deleted file mode 100644
index 579e341fec..0000000000
--- a/MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Common.Net.Handlers
-{
- public abstract class BaseEmbeddedResourceHandler : BaseHandler
- {
- protected BaseEmbeddedResourceHandler(string resourcePath)
- : base()
- {
- ResourcePath = resourcePath;
- }
-
- protected string ResourcePath { get; set; }
-
- protected override Task WriteResponseToOutputStream(Stream stream)
- {
- return GetEmbeddedResourceStream().CopyToAsync(stream);
- }
-
- protected abstract Stream GetEmbeddedResourceStream();
- }
-}
diff --git a/MediaBrowser.Common/Net/Handlers/BaseHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseHandler.cs
index a5058e6caf..95c86e6f7a 100644
--- a/MediaBrowser.Common/Net/Handlers/BaseHandler.cs
+++ b/MediaBrowser.Common/Net/Handlers/BaseHandler.cs
@@ -1,430 +1,826 @@
-using MediaBrowser.Common.Logging;
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Common.Net.Handlers
-{
- public abstract class BaseHandler
- {
- public abstract bool HandlesRequest(HttpListenerRequest request);
-
- private Stream CompressedStream { get; set; }
-
- public virtual bool? UseChunkedEncoding
- {
- get
- {
- return null;
- }
- }
-
- private bool _totalContentLengthDiscovered;
- private long? _totalContentLength;
- public long? TotalContentLength
- {
- get
- {
- if (!_totalContentLengthDiscovered)
- {
- _totalContentLength = GetTotalContentLength();
- _totalContentLengthDiscovered = true;
- }
-
- return _totalContentLength;
- }
- }
-
- protected virtual bool SupportsByteRangeRequests
- {
- get
- {
- return false;
- }
- }
-
- /// <summary>
- /// The original HttpListenerContext
- /// </summary>
- protected HttpListenerContext HttpListenerContext { get; set; }
-
- /// <summary>
- /// The original QueryString
- /// </summary>
- protected NameValueCollection QueryString
- {
- get
- {
- return HttpListenerContext.Request.QueryString;
- }
- }
-
- private List<KeyValuePair<long, long?>> _requestedRanges;
- protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
- {
- get
- {
- if (_requestedRanges == null)
- {
- _requestedRanges = new List<KeyValuePair<long, long?>>();
-
- if (IsRangeRequest)
- {
- // Example: bytes=0-,32-63
- string[] ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
-
- foreach (string range in ranges)
- {
- string[] vals = range.Split('-');
-
- long start = 0;
- long? end = null;
-
- if (!string.IsNullOrEmpty(vals[0]))
- {
- start = long.Parse(vals[0]);
- }
- if (!string.IsNullOrEmpty(vals[1]))
- {
- end = long.Parse(vals[1]);
- }
-
- _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
- }
- }
- }
-
- return _requestedRanges;
- }
- }
-
- protected bool IsRangeRequest
- {
- get
- {
- return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
- }
- }
-
- private bool ClientSupportsCompression
- {
- get
- {
- string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
-
- return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
- }
- }
-
- private string CompressionMethod
- {
- get
- {
- string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
-
- if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return "deflate";
- }
- if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return "gzip";
- }
-
- return null;
- }
- }
-
- public virtual async Task ProcessRequest(HttpListenerContext ctx)
- {
- HttpListenerContext = ctx;
-
- string url = ctx.Request.Url.ToString();
- Logger.LogInfo("Http Server received request at: " + url);
- Logger.LogInfo("Http Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k])));
-
- ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
-
- ctx.Response.KeepAlive = true;
-
- try
- {
- if (SupportsByteRangeRequests && IsRangeRequest)
- {
- ctx.Response.Headers["Accept-Ranges"] = "bytes";
- }
-
- ResponseInfo responseInfo = await GetResponseInfo().ConfigureAwait(false);
-
- if (responseInfo.IsResponseValid)
- {
- // Set the initial status code
- // When serving a range request, we need to return status code 206 to indicate a partial response body
- responseInfo.StatusCode = SupportsByteRangeRequests && IsRangeRequest ? 206 : 200;
- }
-
- ctx.Response.ContentType = responseInfo.ContentType;
-
- if (!string.IsNullOrEmpty(responseInfo.Etag))
- {
- ctx.Response.Headers["ETag"] = responseInfo.Etag;
- }
-
- if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
- {
- DateTime ifModifiedSince;
-
- if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"], out ifModifiedSince))
- {
- // If the cache hasn't expired yet just return a 304
- if (IsCacheValid(ifModifiedSince.ToUniversalTime(), responseInfo.CacheDuration, responseInfo.DateLastModified))
- {
- // ETag must also match (if supplied)
- if ((responseInfo.Etag ?? string.Empty).Equals(ctx.Request.Headers["If-None-Match"] ?? string.Empty))
- {
- responseInfo.StatusCode = 304;
- }
- }
- }
- }
-
- Logger.LogInfo("Responding with status code {0} for url {1}", responseInfo.StatusCode, url);
-
- if (responseInfo.IsResponseValid)
- {
- await ProcessUncachedRequest(ctx, responseInfo).ConfigureAwait(false);
- }
- else
- {
- ctx.Response.StatusCode = responseInfo.StatusCode;
- ctx.Response.SendChunked = false;
- }
- }
- catch (Exception ex)
- {
- // It might be too late if some response data has already been transmitted, but try to set this
- ctx.Response.StatusCode = 500;
-
- Logger.LogException(ex);
- }
- finally
- {
- DisposeResponseStream();
- }
- }
-
- private async Task ProcessUncachedRequest(HttpListenerContext ctx, ResponseInfo responseInfo)
- {
- long? totalContentLength = TotalContentLength;
-
- // By default, use chunked encoding if we don't know the content length
- bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
-
- // Don't force this to true. HttpListener will default it to true if supported by the client.
- if (!useChunkedEncoding)
- {
- ctx.Response.SendChunked = false;
- }
-
- // Set the content length, if we know it
- if (totalContentLength.HasValue)
- {
- ctx.Response.ContentLength64 = totalContentLength.Value;
- }
-
- var compressResponse = responseInfo.CompressResponse && ClientSupportsCompression;
-
- // Add the compression header
- if (compressResponse)
- {
- ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
- }
-
- if (responseInfo.DateLastModified.HasValue)
- {
- ctx.Response.Headers[HttpResponseHeader.LastModified] = responseInfo.DateLastModified.Value.ToString("r");
- }
-
- // Add caching headers
- if (responseInfo.CacheDuration.Ticks > 0)
- {
- CacheResponse(ctx.Response, responseInfo.CacheDuration);
- }
-
- // Set the status code
- ctx.Response.StatusCode = responseInfo.StatusCode;
-
- if (responseInfo.IsResponseValid)
- {
- // Finally, write the response data
- Stream outputStream = ctx.Response.OutputStream;
-
- if (compressResponse)
- {
- if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
- {
- CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false);
- }
- else
- {
- CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false);
- }
-
- outputStream = CompressedStream;
- }
-
- await WriteResponseToOutputStream(outputStream).ConfigureAwait(false);
- }
- else
- {
- ctx.Response.SendChunked = false;
- }
- }
-
- private void CacheResponse(HttpListenerResponse response, TimeSpan duration)
- {
- response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(duration.TotalSeconds);
- response.Headers[HttpResponseHeader.Expires] = DateTime.UtcNow.Add(duration).ToString("r");
- }
-
- protected abstract Task WriteResponseToOutputStream(Stream stream);
-
- protected virtual void DisposeResponseStream()
- {
- if (CompressedStream != null)
- {
- CompressedStream.Dispose();
- }
-
- HttpListenerContext.Response.OutputStream.Dispose();
- }
-
- private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
- {
- if (dateModified.HasValue)
- {
- DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
- ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
-
- return lastModified <= ifModifiedSince;
- }
-
- DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
-
- 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>
- private DateTime NormalizeDateForComparison(DateTime date)
- {
- return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
- }
-
- protected virtual long? GetTotalContentLength()
- {
- return null;
- }
-
- protected abstract Task<ResponseInfo> GetResponseInfo();
-
- private Hashtable _formValues;
-
- /// <summary>
- /// Gets a value from form POST data
- /// </summary>
- protected async Task<string> GetFormValue(string name)
- {
- if (_formValues == null)
- {
- _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false);
- }
-
- if (_formValues.ContainsKey(name))
- {
- return _formValues[name].ToString();
- }
-
- return null;
- }
-
- /// <summary>
- /// Extracts form POST data from a request
- /// </summary>
- private async Task<Hashtable> GetFormValues(HttpListenerRequest request)
- {
- var formVars = new Hashtable();
-
- if (request.HasEntityBody)
- {
- if (request.ContentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) != -1)
- {
- using (Stream requestBody = request.InputStream)
- {
- using (var reader = new StreamReader(requestBody, request.ContentEncoding))
- {
- string s = await reader.ReadToEndAsync().ConfigureAwait(false);
-
- string[] pairs = s.Split('&');
-
- for (int x = 0; x < pairs.Length; x++)
- {
- string pair = pairs[x];
-
- int index = pair.IndexOf('=');
-
- if (index != -1)
- {
- string name = pair.Substring(0, index);
- string value = pair.Substring(index + 1);
- formVars.Add(name, value);
- }
- }
- }
- }
- }
- }
-
- return formVars;
- }
- }
-
- public class ResponseInfo
- {
- public string ContentType { get; set; }
- public string Etag { get; set; }
- public DateTime? DateLastModified { get; set; }
- public TimeSpan CacheDuration { get; set; }
- public bool CompressResponse { get; set; }
- public int StatusCode { get; set; }
-
- public ResponseInfo()
- {
- CacheDuration = TimeSpan.FromTicks(0);
-
- CompressResponse = true;
-
- StatusCode = 200;
- }
-
- public bool IsResponseValid
- {
- get
- {
- return StatusCode == 200 || StatusCode == 206;
- }
- }
- }
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+ /// <summary>
+ /// Class BaseHandler
+ /// </summary>
+ public abstract class BaseHandler<TKernelType> : IHttpServerHandler
+ where TKernelType : IKernel
+ {
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ protected ILogger Logger { get; private set; }
+
+ /// <summary>
+ /// Initializes the specified kernel.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public void Initialize(IKernel kernel)
+ {
+ Kernel = (TKernelType)kernel;
+ Logger = SharedLogger.Logger;
+ }
+
+ /// <summary>
+ /// Gets or sets the kernel.
+ /// </summary>
+ /// <value>The kernel.</value>
+ protected TKernelType Kernel { get; private set; }
+
+ /// <summary>
+ /// Gets the URL suffix used to determine if this handler can process a request.
+ /// </summary>
+ /// <value>The URL suffix.</value>
+ protected virtual string UrlSuffix
+ {
+ get
+ {
+ var name = GetType().Name;
+
+ const string srch = "Handler";
+
+ if (name.EndsWith(srch, StringComparison.OrdinalIgnoreCase))
+ {
+ name = name.Substring(0, name.Length - srch.Length);
+ }
+
+ return "api/" + name;
+ }
+ }
+
+ /// <summary>
+ /// Handleses the request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public virtual bool HandlesRequest(HttpListenerRequest request)
+ {
+ var name = '/' + UrlSuffix.TrimStart('/');
+
+ var url = Kernel.WebApplicationName + name;
+
+ return request.Url.LocalPath.EndsWith(url, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets or sets the compressed stream.
+ /// </summary>
+ /// <value>The compressed stream.</value>
+ private Stream CompressedStream { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether [use chunked encoding].
+ /// </summary>
+ /// <value><c>null</c> if [use chunked encoding] contains no value, <c>true</c> if [use chunked encoding]; otherwise, <c>false</c>.</value>
+ public virtual bool? UseChunkedEncoding
+ {
+ get
+ {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// The original HttpListenerContext
+ /// </summary>
+ /// <value>The HTTP listener context.</value>
+ protected HttpListenerContext HttpListenerContext { get; set; }
+
+ /// <summary>
+ /// The _query string
+ /// </summary>
+ private NameValueCollection _queryString;
+ /// <summary>
+ /// The original QueryString
+ /// </summary>
+ /// <value>The query string.</value>
+ public NameValueCollection QueryString
+ {
+ get
+ {
+ // HttpListenerContext.Request.QueryString is not decoded properly
+ return _queryString ?? (_queryString = HttpUtility.ParseQueryString(HttpListenerContext.Request.Url.Query));
+ }
+ }
+
+ /// <summary>
+ /// The _requested ranges
+ /// </summary>
+ private List<KeyValuePair<long, long?>> _requestedRanges;
+ /// <summary>
+ /// Gets the requested ranges.
+ /// </summary>
+ /// <value>The requested ranges.</value>
+ protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
+ {
+ get
+ {
+ if (_requestedRanges == null)
+ {
+ _requestedRanges = new List<KeyValuePair<long, long?>>();
+
+ if (IsRangeRequest)
+ {
+ // Example: bytes=0-,32-63
+ var ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
+
+ foreach (var range in ranges)
+ {
+ var vals = range.Split('-');
+
+ long start = 0;
+ long? end = null;
+
+ if (!string.IsNullOrEmpty(vals[0]))
+ {
+ start = long.Parse(vals[0]);
+ }
+ if (!string.IsNullOrEmpty(vals[1]))
+ {
+ end = long.Parse(vals[1]);
+ }
+
+ _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
+ }
+ }
+ }
+
+ return _requestedRanges;
+ }
+ }
+
+ /// <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 HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [client supports compression].
+ /// </summary>
+ /// <value><c>true</c> if [client supports compression]; otherwise, <c>false</c>.</value>
+ protected bool ClientSupportsCompression
+ {
+ get
+ {
+ var enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
+
+ return enc.Equals("*", StringComparison.OrdinalIgnoreCase) ||
+ enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 ||
+ enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+ }
+
+ /// <summary>
+ /// Gets the compression method.
+ /// </summary>
+ /// <value>The compression method.</value>
+ private string CompressionMethod
+ {
+ get
+ {
+ var enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
+
+ if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.Equals("*", StringComparison.OrdinalIgnoreCase))
+ {
+ return "deflate";
+ }
+ if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return "gzip";
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Processes the request.
+ /// </summary>
+ /// <param name="ctx">The CTX.</param>
+ /// <returns>Task.</returns>
+ public virtual async Task ProcessRequest(HttpListenerContext ctx)
+ {
+ HttpListenerContext = ctx;
+
+ ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
+
+ ctx.Response.KeepAlive = true;
+
+ try
+ {
+ await ProcessRequestInternal(ctx).ConfigureAwait(false);
+ }
+ catch (InvalidOperationException ex)
+ {
+ HandleException(ctx.Response, ex, 422);
+
+ throw;
+ }
+ catch (ResourceNotFoundException ex)
+ {
+ HandleException(ctx.Response, ex, 404);
+
+ throw;
+ }
+ catch (FileNotFoundException ex)
+ {
+ HandleException(ctx.Response, ex, 404);
+
+ throw;
+ }
+ catch (DirectoryNotFoundException ex)
+ {
+ HandleException(ctx.Response, ex, 404);
+
+ throw;
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ HandleException(ctx.Response, ex, 401);
+
+ throw;
+ }
+ catch (ArgumentException ex)
+ {
+ HandleException(ctx.Response, ex, 400);
+
+ throw;
+ }
+ catch (Exception ex)
+ {
+ HandleException(ctx.Response, ex, 500);
+
+ throw;
+ }
+ finally
+ {
+ DisposeResponseStream();
+ }
+ }
+
+ /// <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)
+ {
+ 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";
+
+ Logger.ErrorException("Error processing request", ex);
+
+ if (!string.IsNullOrEmpty(ex.Message))
+ {
+ response.AddHeader("X-Application-Error-Code", ex.Message);
+ }
+
+ var bytes = Encoding.UTF8.GetBytes(ex.Message);
+
+ var stream = CompressedStream ?? response.OutputStream;
+
+ // This could fail, but try to add the stack trace as the body content
+ try
+ {
+ stream.Write(bytes, 0, bytes.Length);
+ }
+ catch (Exception ex1)
+ {
+ Logger.ErrorException("Error dumping stack trace", ex1);
+ }
+ }
+
+ /// <summary>
+ /// Processes the request internal.
+ /// </summary>
+ /// <param name="ctx">The CTX.</param>
+ /// <returns>Task.</returns>
+ private async Task ProcessRequestInternal(HttpListenerContext ctx)
+ {
+ var responseInfo = await GetResponseInfo().ConfigureAwait(false);
+
+ // Let the client know if byte range requests are supported or not
+ if (responseInfo.SupportsByteRangeRequests)
+ {
+ ctx.Response.Headers["Accept-Ranges"] = "bytes";
+ }
+ else if (!responseInfo.SupportsByteRangeRequests)
+ {
+ ctx.Response.Headers["Accept-Ranges"] = "none";
+ }
+
+ if (responseInfo.IsResponseValid && responseInfo.SupportsByteRangeRequests && IsRangeRequest)
+ {
+ // Set the initial status code
+ // When serving a range request, we need to return status code 206 to indicate a partial response body
+ responseInfo.StatusCode = 206;
+ }
+
+ ctx.Response.ContentType = responseInfo.ContentType;
+
+ if (responseInfo.Etag.HasValue)
+ {
+ ctx.Response.Headers["ETag"] = responseInfo.Etag.Value.ToString("N");
+ }
+
+ var isCacheValid = true;
+
+ // Validate If-Modified-Since
+ if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
+ {
+ DateTime ifModifiedSince;
+
+ if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"], out ifModifiedSince))
+ {
+ isCacheValid = IsCacheValid(ifModifiedSince.ToUniversalTime(), responseInfo.CacheDuration,
+ responseInfo.DateLastModified);
+ }
+ }
+
+ // Validate If-None-Match
+ if (isCacheValid &&
+ (responseInfo.Etag.HasValue || !string.IsNullOrEmpty(ctx.Request.Headers["If-None-Match"])))
+ {
+ Guid ifNoneMatch;
+
+ if (Guid.TryParse(ctx.Request.Headers["If-None-Match"] ?? string.Empty, out ifNoneMatch))
+ {
+ if (responseInfo.Etag.HasValue && responseInfo.Etag.Value == ifNoneMatch)
+ {
+ responseInfo.StatusCode = 304;
+ }
+ }
+ }
+
+ LogResponse(ctx, responseInfo);
+
+ if (responseInfo.IsResponseValid)
+ {
+ await OnProcessingRequest(responseInfo).ConfigureAwait(false);
+ }
+
+ if (responseInfo.IsResponseValid)
+ {
+ await ProcessUncachedRequest(ctx, responseInfo).ConfigureAwait(false);
+ }
+ else
+ {
+ if (responseInfo.StatusCode == 304)
+ {
+ AddAgeHeader(ctx.Response, responseInfo);
+ AddExpiresHeader(ctx.Response, responseInfo);
+ }
+
+ ctx.Response.StatusCode = responseInfo.StatusCode;
+ ctx.Response.SendChunked = false;
+ }
+ }
+
+ /// <summary>
+ /// The _null task result
+ /// </summary>
+ private readonly Task<bool> _nullTaskResult = Task.FromResult(true);
+
+ /// <summary>
+ /// Called when [processing request].
+ /// </summary>
+ /// <param name="responseInfo">The response info.</param>
+ /// <returns>Task.</returns>
+ protected virtual Task OnProcessingRequest(ResponseInfo responseInfo)
+ {
+ return _nullTaskResult;
+ }
+
+ /// <summary>
+ /// Logs the response.
+ /// </summary>
+ /// <param name="ctx">The CTX.</param>
+ /// <param name="responseInfo">The response info.</param>
+ private void LogResponse(HttpListenerContext ctx, ResponseInfo responseInfo)
+ {
+ // Don't log normal 200's
+ if (responseInfo.StatusCode == 200)
+ {
+ return;
+ }
+
+ 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 (" + responseInfo.StatusCode + ") to " + ctx.Request.RemoteEndPoint;
+
+ if (Kernel.Configuration.EnableHttpLevelLogging)
+ {
+ Logger.LogMultiline(msg, LogSeverity.Debug, log);
+ }
+ }
+
+ /// <summary>
+ /// Processes the uncached request.
+ /// </summary>
+ /// <param name="ctx">The CTX.</param>
+ /// <param name="responseInfo">The response info.</param>
+ /// <returns>Task.</returns>
+ private async Task ProcessUncachedRequest(HttpListenerContext ctx, ResponseInfo responseInfo)
+ {
+ var totalContentLength = GetTotalContentLength(responseInfo);
+
+ // By default, use chunked encoding if we don't know the content length
+ var useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
+
+ // Don't force this to true. HttpListener will default it to true if supported by the client.
+ if (!useChunkedEncoding)
+ {
+ ctx.Response.SendChunked = false;
+ }
+
+ // Set the content length, if we know it
+ if (totalContentLength.HasValue)
+ {
+ ctx.Response.ContentLength64 = totalContentLength.Value;
+ }
+
+ var compressResponse = responseInfo.CompressResponse && ClientSupportsCompression;
+
+ // Add the compression header
+ if (compressResponse)
+ {
+ ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
+ ctx.Response.AddHeader("Vary", "Accept-Encoding");
+ }
+
+ // 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 (responseInfo.DateLastModified.HasValue && (!responseInfo.Etag.HasValue || responseInfo.CacheDuration.Ticks > 0))
+ {
+ ctx.Response.Headers[HttpResponseHeader.LastModified] = responseInfo.DateLastModified.Value.ToString("r");
+ AddAgeHeader(ctx.Response, responseInfo);
+ }
+
+ // Add caching headers
+ ConfigureCaching(ctx.Response, responseInfo);
+
+ // Set the status code
+ ctx.Response.StatusCode = responseInfo.StatusCode;
+
+ if (responseInfo.IsResponseValid)
+ {
+ // Finally, write the response data
+ var outputStream = ctx.Response.OutputStream;
+
+ if (compressResponse)
+ {
+ if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
+ {
+ CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, true);
+ }
+ else
+ {
+ CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, true);
+ }
+
+ outputStream = CompressedStream;
+ }
+
+ await WriteResponseToOutputStream(outputStream, responseInfo, totalContentLength).ConfigureAwait(false);
+ }
+ else
+ {
+ ctx.Response.SendChunked = false;
+ }
+ }
+
+ /// <summary>
+ /// Configures the caching.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ /// <param name="responseInfo">The response info.</param>
+ private void ConfigureCaching(HttpListenerResponse response, ResponseInfo responseInfo)
+ {
+ if (responseInfo.CacheDuration.Ticks > 0)
+ {
+ response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(responseInfo.CacheDuration.TotalSeconds);
+ }
+ else if (responseInfo.Etag.HasValue)
+ {
+ response.Headers[HttpResponseHeader.CacheControl] = "public";
+ }
+ else
+ {
+ response.Headers[HttpResponseHeader.CacheControl] = "no-cache, no-store, must-revalidate";
+ response.Headers[HttpResponseHeader.Pragma] = "no-cache, no-store, must-revalidate";
+ }
+
+ AddExpiresHeader(response, responseInfo);
+ }
+
+ /// <summary>
+ /// Adds the expires header.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ /// <param name="responseInfo">The response info.</param>
+ private void AddExpiresHeader(HttpListenerResponse response, ResponseInfo responseInfo)
+ {
+ if (responseInfo.CacheDuration.Ticks > 0)
+ {
+ response.Headers[HttpResponseHeader.Expires] = DateTime.UtcNow.Add(responseInfo.CacheDuration).ToString("r");
+ }
+ else if (!responseInfo.Etag.HasValue)
+ {
+ response.Headers[HttpResponseHeader.Expires] = "-1";
+ }
+ }
+
+ /// <summary>
+ /// Adds the age header.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ /// <param name="responseInfo">The response info.</param>
+ private void AddAgeHeader(HttpListenerResponse response, ResponseInfo responseInfo)
+ {
+ if (responseInfo.DateLastModified.HasValue)
+ {
+ response.Headers[HttpResponseHeader.Age] = Convert.ToInt32((DateTime.UtcNow - responseInfo.DateLastModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
+ }
+ }
+
+ /// <summary>
+ /// Writes the response to output stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ /// <param name="responseInfo">The response info.</param>
+ /// <param name="contentLength">Length of the content.</param>
+ /// <returns>Task.</returns>
+ protected abstract Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? contentLength);
+
+ /// <summary>
+ /// Disposes the response stream.
+ /// </summary>
+ protected virtual void DisposeResponseStream()
+ {
+ if (CompressedStream != null)
+ {
+ try
+ {
+ CompressedStream.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error disposing compressed stream", ex);
+ }
+ }
+
+ try
+ {
+ //HttpListenerContext.Response.OutputStream.Dispose();
+ HttpListenerContext.Response.Close();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error disposing response", ex);
+ }
+ }
+
+ /// <summary>
+ /// Determines whether [is cache valid] [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 cache valid] [the specified if modified since]; otherwise, <c>false</c>.</returns>
+ private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
+ {
+ if (dateModified.HasValue)
+ {
+ DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
+ ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
+
+ return lastModified <= ifModifiedSince;
+ }
+
+ DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
+
+ 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>
+ /// Gets the total length of the content.
+ /// </summary>
+ /// <param name="responseInfo">The response info.</param>
+ /// <returns>System.Nullable{System.Int64}.</returns>
+ protected virtual long? GetTotalContentLength(ResponseInfo responseInfo)
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the response info.
+ /// </summary>
+ /// <returns>Task{ResponseInfo}.</returns>
+ protected abstract Task<ResponseInfo> GetResponseInfo();
+
+ /// <summary>
+ /// Gets a bool query string param.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected bool GetBoolQueryStringParam(string name)
+ {
+ var val = QueryString[name] ?? string.Empty;
+
+ return val.Equals("1", StringComparison.OrdinalIgnoreCase) || val.Equals("true", StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// The _form values
+ /// </summary>
+ private Hashtable _formValues;
+
+ /// <summary>
+ /// Gets a value from form POST data
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{System.String}.</returns>
+ protected async Task<string> GetFormValue(string name)
+ {
+ if (_formValues == null)
+ {
+ _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false);
+ }
+
+ if (_formValues.ContainsKey(name))
+ {
+ return _formValues[name].ToString();
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Extracts form POST data from a request
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>Task{Hashtable}.</returns>
+ private async Task<Hashtable> GetFormValues(HttpListenerRequest request)
+ {
+ var formVars = new Hashtable();
+
+ if (request.HasEntityBody)
+ {
+ if (request.ContentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ using (var requestBody = request.InputStream)
+ {
+ using (var reader = new StreamReader(requestBody, request.ContentEncoding))
+ {
+ var s = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+ var pairs = s.Split('&');
+
+ foreach (var pair in pairs)
+ {
+ var index = pair.IndexOf('=');
+
+ if (index != -1)
+ {
+ var name = pair.Substring(0, index);
+ var value = pair.Substring(index + 1);
+ formVars.Add(name, value);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return formVars;
+ }
+ }
+
+ internal static class SharedLogger
+ {
+ /// <summary>
+ /// The logger
+ /// </summary>
+ internal static ILogger Logger = LogManager.GetLogger("Http Handler");
+ }
+
+ /// <summary>
+ /// Class ResponseInfo
+ /// </summary>
+ public class ResponseInfo
+ {
+ /// <summary>
+ /// Gets or sets the type of the content.
+ /// </summary>
+ /// <value>The type of the content.</value>
+ public string ContentType { get; set; }
+ /// <summary>
+ /// Gets or sets the etag.
+ /// </summary>
+ /// <value>The etag.</value>
+ public Guid? Etag { get; set; }
+ /// <summary>
+ /// Gets or sets the date last modified.
+ /// </summary>
+ /// <value>The date last modified.</value>
+ public DateTime? DateLastModified { get; set; }
+ /// <summary>
+ /// Gets or sets the duration of the cache.
+ /// </summary>
+ /// <value>The duration of the cache.</value>
+ public TimeSpan CacheDuration { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [compress response].
+ /// </summary>
+ /// <value><c>true</c> if [compress response]; otherwise, <c>false</c>.</value>
+ public bool CompressResponse { get; set; }
+ /// <summary>
+ /// Gets or sets the status code.
+ /// </summary>
+ /// <value>The status code.</value>
+ public int StatusCode { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports byte range requests].
+ /// </summary>
+ /// <value><c>true</c> if [supports byte range requests]; otherwise, <c>false</c>.</value>
+ public bool SupportsByteRangeRequests { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ResponseInfo" /> class.
+ /// </summary>
+ public ResponseInfo()
+ {
+ CacheDuration = TimeSpan.FromTicks(0);
+
+ CompressResponse = true;
+
+ StatusCode = 200;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is response valid.
+ /// </summary>
+ /// <value><c>true</c> if this instance is response valid; otherwise, <c>false</c>.</value>
+ public bool IsResponseValid
+ {
+ get
+ {
+ return StatusCode >= 200 && StatusCode < 300;
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs
index 53b3ee817f..293cb6e98a 100644
--- a/MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs
+++ b/MediaBrowser.Common/Net/Handlers/BaseSerializationHandler.cs
@@ -1,90 +1,134 @@
-using MediaBrowser.Common.Serialization;
-using System;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Common.Net.Handlers
-{
- public abstract class BaseSerializationHandler<T> : BaseHandler
- where T : class
- {
- public SerializationFormat SerializationFormat
- {
- get
- {
- string format = QueryString["dataformat"];
-
- if (string.IsNullOrEmpty(format))
- {
- return SerializationFormat.Json;
- }
-
- return (SerializationFormat)Enum.Parse(typeof(SerializationFormat), format, true);
- }
- }
-
- protected string ContentType
- {
- get
- {
- switch (SerializationFormat)
- {
- case SerializationFormat.Jsv:
- return "text/plain";
- case SerializationFormat.Protobuf:
- return "application/x-protobuf";
- default:
- return MimeTypes.JsonMimeType;
- }
- }
- }
-
- protected override async Task<ResponseInfo> GetResponseInfo()
- {
- ResponseInfo info = new ResponseInfo
- {
- ContentType = ContentType
- };
-
- _objectToSerialize = await GetObjectToSerialize().ConfigureAwait(false);
-
- if (_objectToSerialize == null)
- {
- info.StatusCode = 404;
- }
-
- return info;
- }
-
- private T _objectToSerialize;
-
- protected abstract Task<T> GetObjectToSerialize();
-
- protected override Task WriteResponseToOutputStream(Stream stream)
- {
- return Task.Run(() =>
- {
- switch (SerializationFormat)
- {
- case SerializationFormat.Jsv:
- JsvSerializer.SerializeToStream(_objectToSerialize, stream);
- break;
- case SerializationFormat.Protobuf:
- ProtobufSerializer.SerializeToStream(_objectToSerialize, stream);
- break;
- default:
- JsonSerializer.SerializeToStream(_objectToSerialize, stream);
- break;
- }
- });
- }
- }
-
- public enum SerializationFormat
- {
- Json,
- Jsv,
- Protobuf
- }
-
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Common.Serialization;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+ /// <summary>
+ /// Class BaseSerializationHandler
+ /// </summary>
+ /// <typeparam name="TKernelType">The type of the T kernel type.</typeparam>
+ /// <typeparam name="T"></typeparam>
+ public abstract class BaseSerializationHandler<TKernelType, T> : BaseHandler<TKernelType>
+ where TKernelType : IKernel
+ where T : class
+ {
+ /// <summary>
+ /// Gets the serialization format.
+ /// </summary>
+ /// <value>The serialization format.</value>
+ public SerializationFormat SerializationFormat
+ {
+ get
+ {
+ var format = QueryString["dataformat"];
+
+ if (string.IsNullOrEmpty(format))
+ {
+ return SerializationFormat.Json;
+ }
+
+ return (SerializationFormat)Enum.Parse(typeof(SerializationFormat), format, true);
+ }
+ }
+
+ /// <summary>
+ /// Gets the type of the content.
+ /// </summary>
+ /// <value>The type of the content.</value>
+ protected string ContentType
+ {
+ get
+ {
+ switch (SerializationFormat)
+ {
+ case SerializationFormat.Protobuf:
+ return "application/x-protobuf";
+ default:
+ return MimeTypes.JsonMimeType;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the response info.
+ /// </summary>
+ /// <returns>Task{ResponseInfo}.</returns>
+ protected override Task<ResponseInfo> GetResponseInfo()
+ {
+ return Task.FromResult(new ResponseInfo
+ {
+ ContentType = ContentType
+ });
+ }
+
+ /// <summary>
+ /// Called when [processing request].
+ /// </summary>
+ /// <param name="responseInfo">The response info.</param>
+ /// <returns>Task.</returns>
+ protected override async Task OnProcessingRequest(ResponseInfo responseInfo)
+ {
+ _objectToSerialize = await GetObjectToSerialize().ConfigureAwait(false);
+
+ if (_objectToSerialize == null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ await base.OnProcessingRequest(responseInfo).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// The _object to serialize
+ /// </summary>
+ private T _objectToSerialize;
+
+ /// <summary>
+ /// Gets the object to serialize.
+ /// </summary>
+ /// <returns>Task{`0}.</returns>
+ protected abstract Task<T> GetObjectToSerialize();
+
+ /// <summary>
+ /// Writes the response to output stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ /// <param name="responseInfo">The response info.</param>
+ /// <param name="contentLength">Length of the content.</param>
+ /// <returns>Task.</returns>
+ protected override Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? contentLength)
+ {
+ return Task.Run(() =>
+ {
+ switch (SerializationFormat)
+ {
+ case SerializationFormat.Protobuf:
+ Kernel.ProtobufSerializer.SerializeToStream(_objectToSerialize, stream);
+ break;
+ default:
+ JsonSerializer.SerializeToStream(_objectToSerialize, stream);
+ break;
+ }
+ });
+ }
+ }
+
+ /// <summary>
+ /// Enum SerializationFormat
+ /// </summary>
+ public enum SerializationFormat
+ {
+ /// <summary>
+ /// The json
+ /// </summary>
+ Json,
+ /// <summary>
+ /// The protobuf
+ /// </summary>
+ Protobuf
+ }
+}
diff --git a/MediaBrowser.Common/Net/Handlers/IHttpServerHandler.cs b/MediaBrowser.Common/Net/Handlers/IHttpServerHandler.cs
new file mode 100644
index 0000000000..dadd614737
--- /dev/null
+++ b/MediaBrowser.Common/Net/Handlers/IHttpServerHandler.cs
@@ -0,0 +1,32 @@
+using MediaBrowser.Common.Kernel;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+ /// <summary>
+ /// Interface IHttpServerHandler
+ /// </summary>
+ public interface IHttpServerHandler
+ {
+ /// <summary>
+ /// Initializes the specified kernel.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ void Initialize(IKernel kernel);
+
+ /// <summary>
+ /// Handleses the request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ bool HandlesRequest(HttpListenerRequest request);
+
+ /// <summary>
+ /// Processes the request.
+ /// </summary>
+ /// <param name="ctx">The CTX.</param>
+ /// <returns>Task.</returns>
+ Task ProcessRequest(HttpListenerContext ctx);
+ }
+}
diff --git a/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs b/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs
index 11438b164b..3967d15c35 100644
--- a/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs
+++ b/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs
@@ -1,249 +1,264 @@
-using MediaBrowser.Common.Logging;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Common.Net.Handlers
-{
- public class StaticFileHandler : BaseHandler
- {
- public override bool HandlesRequest(HttpListenerRequest request)
- {
- return false;
- }
-
- private string _path;
- public virtual string Path
- {
- get
- {
- if (!string.IsNullOrWhiteSpace(_path))
- {
- return _path;
- }
-
- return QueryString["path"];
- }
- set
- {
- _path = value;
- }
- }
-
- private Stream SourceStream { get; set; }
-
- protected override bool SupportsByteRangeRequests
- {
- get
- {
- return true;
- }
- }
-
- private bool ShouldCompressResponse(string contentType)
- {
- // Can't compress these
- if (IsRangeRequest)
- {
- return false;
- }
-
- // Don't compress media
- if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
-
- // It will take some work to support compression within this handler
- return false;
- }
-
- protected override long? GetTotalContentLength()
- {
- return SourceStream.Length;
- }
-
- protected override Task<ResponseInfo> GetResponseInfo()
- {
- ResponseInfo info = new ResponseInfo
- {
- ContentType = MimeTypes.GetMimeType(Path),
- };
-
- try
- {
- SourceStream = File.OpenRead(Path);
- }
- catch (FileNotFoundException ex)
- {
- info.StatusCode = 404;
- Logger.LogException(ex);
- }
- catch (DirectoryNotFoundException ex)
- {
- info.StatusCode = 404;
- Logger.LogException(ex);
- }
- catch (UnauthorizedAccessException ex)
- {
- info.StatusCode = 403;
- Logger.LogException(ex);
- }
-
- info.CompressResponse = ShouldCompressResponse(info.ContentType);
-
- if (SourceStream != null)
- {
- info.DateLastModified = File.GetLastWriteTimeUtc(Path);
- }
-
- return Task.FromResult<ResponseInfo>(info);
- }
-
- protected override Task WriteResponseToOutputStream(Stream stream)
- {
- if (IsRangeRequest)
- {
- KeyValuePair<long, long?> requestedRange = RequestedRanges.First();
-
- // If the requested range is "0-" and we know the total length, we can optimize by avoiding having to buffer the content into memory
- if (requestedRange.Value == null && TotalContentLength != null)
- {
- return ServeCompleteRangeRequest(requestedRange, stream);
- }
- if (TotalContentLength.HasValue)
- {
- // This will have to buffer a portion of the content into memory
- return ServePartialRangeRequestWithKnownTotalContentLength(requestedRange, stream);
- }
-
- // This will have to buffer the entire content into memory
- return ServePartialRangeRequestWithUnknownTotalContentLength(requestedRange, stream);
- }
-
- return SourceStream.CopyToAsync(stream);
- }
-
- protected override void DisposeResponseStream()
- {
- base.DisposeResponseStream();
-
- if (SourceStream != null)
- {
- SourceStream.Dispose();
- }
- }
-
- /// <summary>
- /// Handles a range request of "bytes=0-"
- /// This will serve the complete content and add the content-range header
- /// </summary>
- private Task ServeCompleteRangeRequest(KeyValuePair<long, long?> requestedRange, Stream responseStream)
- {
- long totalContentLength = TotalContentLength.Value;
-
- long rangeStart = requestedRange.Key;
- long rangeEnd = totalContentLength - 1;
- long rangeLength = 1 + rangeEnd - rangeStart;
-
- // Content-Length is the length of what we're serving, not the original content
- HttpListenerContext.Response.ContentLength64 = rangeLength;
- HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
-
- if (rangeStart > 0)
- {
- SourceStream.Position = rangeStart;
- }
-
- return SourceStream.CopyToAsync(responseStream);
- }
-
- /// <summary>
- /// Serves a partial range request where the total content length is not known
- /// </summary>
- private async Task ServePartialRangeRequestWithUnknownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
- {
- // Read the entire stream so that we can determine the length
- byte[] bytes = await ReadBytes(SourceStream, 0, null).ConfigureAwait(false);
-
- long totalContentLength = bytes.LongLength;
-
- long rangeStart = requestedRange.Key;
- long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
- long rangeLength = 1 + rangeEnd - rangeStart;
-
- // Content-Length is the length of what we're serving, not the original content
- HttpListenerContext.Response.ContentLength64 = rangeLength;
- HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
-
- await responseStream.WriteAsync(bytes, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength)).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Serves a partial range request where the total content length is already known
- /// </summary>
- private async Task ServePartialRangeRequestWithKnownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
- {
- long totalContentLength = TotalContentLength.Value;
- long rangeStart = requestedRange.Key;
- long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
- long rangeLength = 1 + rangeEnd - rangeStart;
-
- // Only read the bytes we need
- byte[] bytes = await ReadBytes(SourceStream, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength)).ConfigureAwait(false);
-
- // Content-Length is the length of what we're serving, not the original content
- HttpListenerContext.Response.ContentLength64 = rangeLength;
-
- HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
-
- await responseStream.WriteAsync(bytes, 0, Convert.ToInt32(rangeLength)).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Reads bytes from a stream
- /// </summary>
- /// <param name="input">The input stream</param>
- /// <param name="start">The starting position</param>
- /// <param name="count">The number of bytes to read, or null to read to the end.</param>
- private async Task<byte[]> ReadBytes(Stream input, int start, int? count)
- {
- if (start > 0)
- {
- input.Position = start;
- }
-
- if (count == null)
- {
- var buffer = new byte[16 * 1024];
-
- using (var ms = new MemoryStream())
- {
- int read;
- while ((read = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
- {
- await ms.WriteAsync(buffer, 0, read).ConfigureAwait(false);
- }
- return ms.ToArray();
- }
- }
- else
- {
- var buffer = new byte[count.Value];
-
- using (var ms = new MemoryStream())
- {
- int read = await input.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
-
- await ms.WriteAsync(buffer, 0, read).ConfigureAwait(false);
-
- return ms.ToArray();
- }
- }
-
- }
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Kernel;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+ /// <summary>
+ /// Represents an http handler that serves static content
+ /// </summary>
+ public class StaticFileHandler : BaseHandler<IKernel>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StaticFileHandler" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public StaticFileHandler(IKernel kernel)
+ {
+ Initialize(kernel);
+ }
+
+ /// <summary>
+ /// The _path
+ /// </summary>
+ private string _path;
+ /// <summary>
+ /// Gets or sets the path to the static resource
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path
+ {
+ get
+ {
+ if (!string.IsNullOrWhiteSpace(_path))
+ {
+ return _path;
+ }
+
+ return QueryString["path"];
+ }
+ set
+ {
+ _path = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the last date modified of the resource
+ /// </summary>
+ /// <value>The last date modified.</value>
+ public DateTime? LastDateModified { get; set; }
+
+ /// <summary>
+ /// Gets or sets the content type of the resource
+ /// </summary>
+ /// <value>The type of the content.</value>
+ public string ContentType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the content type of the resource
+ /// </summary>
+ /// <value>The etag.</value>
+ public Guid Etag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the source stream of the resource
+ /// </summary>
+ /// <value>The source stream.</value>
+ public Stream SourceStream { get; set; }
+
+ /// <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;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Gets or sets the duration of the cache.
+ /// </summary>
+ /// <value>The duration of the cache.</value>
+ public TimeSpan? CacheDuration { get; set; }
+
+ /// <summary>
+ /// Gets the total length of the content.
+ /// </summary>
+ /// <param name="responseInfo">The response info.</param>
+ /// <returns>System.Nullable{System.Int64}.</returns>
+ protected override long? GetTotalContentLength(ResponseInfo responseInfo)
+ {
+ // If we're compressing the response, content length must be the compressed length, which we don't know
+ if (responseInfo.CompressResponse && ClientSupportsCompression)
+ {
+ return null;
+ }
+
+ return SourceStream.Length;
+ }
+
+ /// <summary>
+ /// Gets the response info.
+ /// </summary>
+ /// <returns>Task{ResponseInfo}.</returns>
+ protected override Task<ResponseInfo> GetResponseInfo()
+ {
+ var info = new ResponseInfo
+ {
+ ContentType = ContentType ?? MimeTypes.GetMimeType(Path),
+ Etag = Etag,
+ DateLastModified = LastDateModified
+ };
+
+ if (SourceStream == null && !string.IsNullOrEmpty(Path))
+ {
+ // FileShare must be ReadWrite in case someone else is currently writing to it.
+ SourceStream = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous);
+ }
+
+ info.CompressResponse = ShouldCompressResponse(info.ContentType);
+
+ info.SupportsByteRangeRequests = !info.CompressResponse || !ClientSupportsCompression;
+
+ if (!info.DateLastModified.HasValue && !string.IsNullOrWhiteSpace(Path))
+ {
+ info.DateLastModified = File.GetLastWriteTimeUtc(Path);
+ }
+
+ if (CacheDuration.HasValue)
+ {
+ info.CacheDuration = CacheDuration.Value;
+ }
+
+ if (SourceStream == null && string.IsNullOrEmpty(Path))
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ return Task.FromResult(info);
+ }
+
+ /// <summary>
+ /// Writes the response to output stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ /// <param name="responseInfo">The response info.</param>
+ /// <param name="totalContentLength">Total length of the content.</param>
+ /// <returns>Task.</returns>
+ protected override Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? totalContentLength)
+ {
+ if (IsRangeRequest && totalContentLength.HasValue)
+ {
+ var requestedRange = RequestedRanges.First();
+
+ // If the requested range is "0-", we can optimize by just doing a stream copy
+ if (!requestedRange.Value.HasValue)
+ {
+ return ServeCompleteRangeRequest(requestedRange, stream, totalContentLength.Value);
+ }
+
+ // This will have to buffer a portion of the content into memory
+ return ServePartialRangeRequest(requestedRange.Key, requestedRange.Value.Value, stream, totalContentLength.Value);
+ }
+
+ return SourceStream.CopyToAsync(stream);
+ }
+
+ /// <summary>
+ /// Disposes the response stream.
+ /// </summary>
+ protected override void DisposeResponseStream()
+ {
+ if (SourceStream != null)
+ {
+ SourceStream.Dispose();
+ }
+
+ base.DisposeResponseStream();
+ }
+
+ /// <summary>
+ /// Handles a range request of "bytes=0-"
+ /// This will serve the complete content and add the content-range header
+ /// </summary>
+ /// <param name="requestedRange">The requested range.</param>
+ /// <param name="responseStream">The response stream.</param>
+ /// <param name="totalContentLength">Total length of the content.</param>
+ /// <returns>Task.</returns>
+ private Task ServeCompleteRangeRequest(KeyValuePair<long, long?> requestedRange, Stream responseStream, long totalContentLength)
+ {
+ var rangeStart = requestedRange.Key;
+ var rangeEnd = totalContentLength - 1;
+ var rangeLength = 1 + rangeEnd - rangeStart;
+
+ // Content-Length is the length of what we're serving, not the original content
+ HttpListenerContext.Response.ContentLength64 = rangeLength;
+ HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+ if (rangeStart > 0)
+ {
+ SourceStream.Position = rangeStart;
+ }
+
+ return SourceStream.CopyToAsync(responseStream);
+ }
+
+ /// <summary>
+ /// Serves a partial range request
+ /// </summary>
+ /// <param name="rangeStart">The range start.</param>
+ /// <param name="rangeEnd">The range end.</param>
+ /// <param name="responseStream">The response stream.</param>
+ /// <param name="totalContentLength">Total length of the content.</param>
+ /// <returns>Task.</returns>
+ private async Task ServePartialRangeRequest(long rangeStart, long rangeEnd, Stream responseStream, long totalContentLength)
+ {
+ var rangeLength = 1 + rangeEnd - rangeStart;
+
+ // Content-Length is the length of what we're serving, not the original content
+ HttpListenerContext.Response.ContentLength64 = rangeLength;
+ HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+ SourceStream.Position = rangeStart;
+
+ // Fast track to just copy the stream to the end
+ if (rangeEnd == totalContentLength - 1)
+ {
+ await SourceStream.CopyToAsync(responseStream).ConfigureAwait(false);
+ }
+ else
+ {
+ // Read the bytes we need
+ var buffer = new byte[Convert.ToInt32(rangeLength)];
+ await SourceStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+
+ await responseStream.WriteAsync(buffer, 0, Convert.ToInt32(rangeLength)).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Net/HttpManager.cs b/MediaBrowser.Common/Net/HttpManager.cs
new file mode 100644
index 0000000000..3a87a89514
--- /dev/null
+++ b/MediaBrowser.Common/Net/HttpManager.cs
@@ -0,0 +1,452 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Cache;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Class HttpManager
+ /// </summary>
+ public class HttpManager : BaseManager<IKernel>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public HttpManager(IKernel kernel)
+ : base(kernel)
+ {
+ }
+
+ /// <summary>
+ /// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
+ /// DON'T dispose it after use.
+ /// </summary>
+ /// <value>The HTTP clients.</value>
+ private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>();
+
+ /// <summary>
+ /// Gets
+ /// </summary>
+ /// <param name="host">The host.</param>
+ /// <returns>HttpClient.</returns>
+ /// <exception cref="System.ArgumentNullException">host</exception>
+ private HttpClient GetHttpClient(string host)
+ {
+ if (string.IsNullOrEmpty(host))
+ {
+ throw new ArgumentNullException("host");
+ }
+
+ HttpClient client;
+ if (!_httpClients.TryGetValue(host, out client))
+ {
+ var handler = new WebRequestHandler
+ {
+ AutomaticDecompression = DecompressionMethods.Deflate,
+ CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate)
+ };
+
+ client = new HttpClient(handler);
+ client.DefaultRequestHeaders.Add("Accept", "application/json,image/*");
+ client.Timeout = TimeSpan.FromSeconds(15);
+ _httpClients.TryAdd(host, client);
+ }
+
+ return client;
+ }
+
+ /// <summary>
+ /// Performs a GET request and returns the resulting stream
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="resourcePool">The resource pool.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{Stream}.</returns>
+ /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
+ public async Task<Stream> Get(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+ {
+ ValidateParams(url, resourcePool, cancellationToken);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ Logger.Info("HttpManager.Get url: {0}", url);
+
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var msg = await GetHttpClient(GetHostFromUrl(url)).GetAsync(url, cancellationToken).ConfigureAwait(false);
+
+ EnsureSuccessStatusCode(msg);
+
+ return await msg.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ }
+ catch (OperationCanceledException ex)
+ {
+ throw GetCancellationException(url, cancellationToken, ex);
+ }
+ catch (HttpRequestException ex)
+ {
+ Logger.ErrorException("Error getting response from " + url, ex);
+
+ throw new HttpException(ex.Message, ex);
+ }
+ finally
+ {
+ resourcePool.Release();
+ }
+ }
+
+ /// <summary>
+ /// Performs a POST request
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="postData">Params to add to the POST data.</param>
+ /// <param name="resourcePool">The resource pool.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>stream on success, null on failure</returns>
+ /// <exception cref="System.ArgumentNullException">postData</exception>
+ /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
+ public async Task<Stream> Post(string url, Dictionary<string, string> postData, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+ {
+ ValidateParams(url, resourcePool, cancellationToken);
+
+ if (postData == null)
+ {
+ throw new ArgumentNullException("postData");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var strings = postData.Keys.Select(key => string.Format("{0}={1}", key, postData[key]));
+ var postContent = string.Join("&", strings.ToArray());
+ var content = new StringContent(postContent, Encoding.UTF8, "application/x-www-form-urlencoded");
+
+ await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ Logger.Info("HttpManager.Post url: {0}", url);
+
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var msg = await GetHttpClient(GetHostFromUrl(url)).PostAsync(url, content, cancellationToken).ConfigureAwait(false);
+
+ EnsureSuccessStatusCode(msg);
+
+ return await msg.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ }
+ catch (OperationCanceledException ex)
+ {
+ throw GetCancellationException(url, cancellationToken, ex);
+ }
+ catch (HttpRequestException ex)
+ {
+ Logger.ErrorException("Error getting response from " + url, ex);
+
+ throw new HttpException(ex.Message, ex);
+ }
+ finally
+ {
+ resourcePool.Release();
+ }
+ }
+
+ /// <summary>
+ /// Downloads the contents of a given url into a temporary location
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="resourcePool">The resource pool.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="userAgent">The user agent.</param>
+ /// <returns>Task{System.String}.</returns>
+ /// <exception cref="System.ArgumentNullException">progress</exception>
+ /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
+ public async Task<string> FetchToTempFile(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken, IProgress<double> progress, string userAgent = null)
+ {
+ ValidateParams(url, resourcePool, cancellationToken);
+
+ if (progress == null)
+ {
+ throw new ArgumentNullException("progress");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var tempFile = Path.Combine(Kernel.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".tmp");
+
+ var message = new HttpRequestMessage(HttpMethod.Get, url);
+
+ if (!string.IsNullOrEmpty(userAgent))
+ {
+ message.Headers.Add("User-Agent", userAgent);
+ }
+
+ await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ Logger.Info("HttpManager.FetchToTempFile url: {0}, temp file: {1}", url, tempFile);
+
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (var response = await GetHttpClient(GetHostFromUrl(url)).SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false))
+ {
+ EnsureSuccessStatusCode(response);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ IEnumerable<string> lengthValues;
+
+ if (!response.Headers.TryGetValues("content-length", out lengthValues) &&
+ !response.Content.Headers.TryGetValues("content-length", out lengthValues))
+ {
+ // We're not able to track progress
+ using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
+ {
+ using (var fs = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+ {
+ await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ else
+ {
+ var length = long.Parse(string.Join(string.Empty, lengthValues.ToArray()));
+
+ using (var stream = ProgressStream.CreateReadProgressStream(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), progress.Report, length))
+ {
+ using (var fs = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+ {
+ await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+
+ progress.Report(100);
+
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+
+ return tempFile;
+ }
+ catch (OperationCanceledException ex)
+ {
+ // Cleanup
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
+
+ throw GetCancellationException(url, cancellationToken, ex);
+ }
+ catch (HttpRequestException ex)
+ {
+ Logger.ErrorException("Error getting response from " + url, ex);
+
+ // Cleanup
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
+
+ throw new HttpException(ex.Message, ex);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error getting response from " + url, ex);
+
+ // Cleanup
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
+
+ throw;
+ }
+ finally
+ {
+ resourcePool.Release();
+ }
+ }
+
+ /// <summary>
+ /// Downloads the contents of a given url into a MemoryStream
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="resourcePool">The resource pool.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{MemoryStream}.</returns>
+ /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
+ public async Task<MemoryStream> FetchToMemoryStream(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+ {
+ ValidateParams(url, resourcePool, cancellationToken);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var message = new HttpRequestMessage(HttpMethod.Get, url);
+
+ await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ var ms = new MemoryStream();
+
+ Logger.Info("HttpManager.FetchToMemoryStream url: {0}", url);
+
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (var response = await GetHttpClient(GetHostFromUrl(url)).SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false))
+ {
+ EnsureSuccessStatusCode(response);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
+ {
+ await stream.CopyToAsync(ms, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+
+ ms.Position = 0;
+
+ return ms;
+ }
+ catch (OperationCanceledException ex)
+ {
+ ms.Dispose();
+
+ throw GetCancellationException(url, cancellationToken, ex);
+ }
+ catch (HttpRequestException ex)
+ {
+ Logger.ErrorException("Error getting response from " + url, ex);
+
+ ms.Dispose();
+
+ throw new HttpException(ex.Message, ex);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error getting response from " + url, ex);
+
+ ms.Dispose();
+
+ throw;
+ }
+ finally
+ {
+ resourcePool.Release();
+ }
+ }
+
+ /// <summary>
+ /// Validates the params.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="resourcePool">The resource pool.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <exception cref="System.ArgumentNullException">url</exception>
+ private void ValidateParams(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(url))
+ {
+ throw new ArgumentNullException("url");
+ }
+
+ if (resourcePool == null)
+ {
+ throw new ArgumentNullException("resourcePool");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+ }
+
+ /// <summary>
+ /// Gets the host from URL.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <returns>System.String.</returns>
+ private string GetHostFromUrl(string url)
+ {
+ var start = url.IndexOf("://", StringComparison.OrdinalIgnoreCase) + 3;
+ var len = url.IndexOf('/', start) - start;
+ return url.Substring(start, len);
+ }
+
+ /// <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 override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ foreach (var client in _httpClients.Values.ToList())
+ {
+ client.Dispose();
+ }
+
+ _httpClients.Clear();
+ }
+
+ base.Dispose(dispose);
+ }
+
+ /// <summary>
+ /// Throws the cancellation exception.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="exception">The exception.</param>
+ /// <returns>Exception.</returns>
+ private Exception GetCancellationException(string url, CancellationToken cancellationToken, OperationCanceledException exception)
+ {
+ // If the HttpClient's timeout is reached, it will cancel the Task internally
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ var msg = string.Format("Connection to {0} timed out", url);
+
+ Logger.Error(msg);
+
+ // Throw an HttpException so that the caller doesn't think it was cancelled by user code
+ return new HttpException(msg, exception) { IsTimedOut = true };
+ }
+
+ return exception;
+ }
+
+ /// <summary>
+ /// Ensures the success status code.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
+ private void EnsureSuccessStatusCode(HttpResponseMessage response)
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new HttpException(response.ReasonPhrase) { StatusCode = response.StatusCode };
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Net/HttpServer.cs b/MediaBrowser.Common/Net/HttpServer.cs
index 276e14eb31..efd6d9d325 100644
--- a/MediaBrowser.Common/Net/HttpServer.cs
+++ b/MediaBrowser.Common/Net/HttpServer.cs
@@ -1,40 +1,444 @@
-using System;
-using System.Net;
-using System.Reactive.Linq;
-
-namespace MediaBrowser.Common.Net
-{
- public class HttpServer : IObservable<HttpListenerContext>, IDisposable
- {
- private readonly HttpListener _listener;
- private readonly IObservable<HttpListenerContext> _stream;
-
- public HttpServer(string url)
- {
- _listener = new HttpListener();
- _listener.Prefixes.Add(url);
- _listener.Start();
- _stream = ObservableHttpContext();
- }
-
- private IObservable<HttpListenerContext> ObservableHttpContext()
- {
- return Observable.Create<HttpListenerContext>(obs =>
- Observable.FromAsync(() => _listener.GetContextAsync())
- .Subscribe(obs))
- .Repeat()
- .Retry()
- .Publish()
- .RefCount();
- }
- public void Dispose()
- {
- _listener.Stop();
- }
-
- public IDisposable Subscribe(IObserver<HttpListenerContext> observer)
- {
- return _stream.Subscribe(observer);
- }
- }
+using Funq;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Model.Logging;
+using ServiceStack.Api.Swagger;
+using ServiceStack.Common.Web;
+using ServiceStack.Logging;
+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.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Reactive.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Class HttpServer
+ /// </summary>
+ public class HttpServer : HttpListenerBase
+ {
+ /// <summary>
+ /// The logger
+ /// </summary>
+ private static ILogger Logger = Logging.LogManager.GetLogger("HttpServer");
+
+ /// <summary>
+ /// Gets the URL prefix.
+ /// </summary>
+ /// <value>The URL prefix.</value>
+ public string UrlPrefix { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the kernel.
+ /// </summary>
+ /// <value>The kernel.</value>
+ private IKernel Kernel { 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>
+ /// Occurs when [web socket connected].
+ /// </summary>
+ public event EventHandler<WebSocketConnectEventArgs> WebSocketConnected;
+
+ /// <summary>
+ /// Gets the default redirect path.
+ /// </summary>
+ /// <value>The default redirect path.</value>
+ public string DefaultRedirectPath { get; private set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpServer" /> class.
+ /// </summary>
+ /// <param name="urlPrefix">The URL.</param>
+ /// <param name="serverName">Name of the product.</param>
+ /// <param name="kernel">The kernel.</param>
+ /// <param name="defaultRedirectpath">The default redirectpath.</param>
+ /// <exception cref="System.ArgumentNullException">urlPrefix</exception>
+ public HttpServer(string urlPrefix, string serverName, IKernel kernel, string defaultRedirectpath = null)
+ : base()
+ {
+ if (string.IsNullOrEmpty(urlPrefix))
+ {
+ throw new ArgumentNullException("urlPrefix");
+ }
+
+ DefaultRedirectPath = defaultRedirectpath;
+
+ EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath = null;
+ EndpointHostConfig.Instance.MetadataRedirectPath = "metadata";
+
+ UrlPrefix = urlPrefix;
+ Kernel = kernel;
+
+ EndpointHost.ConfigureHost(this, serverName, CreateServiceManager());
+
+ ContentTypeFilters.Register(ContentType.ProtoBuf, (reqCtx, res, stream) => Kernel.ProtobufSerializer.SerializeToStream(res, stream), (type, stream) => Kernel.ProtobufSerializer.DeserializeFromStream(stream, type));
+
+ Init();
+ Start(urlPrefix);
+ }
+
+ /// <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>
+ /// Configures the specified container.
+ /// </summary>
+ /// <param name="container">The container.</param>
+ public override void Configure(Container container)
+ {
+ if (!string.IsNullOrEmpty(DefaultRedirectPath))
+ {
+ SetConfig(new EndpointHostConfig
+ {
+ DefaultRedirectPath = DefaultRedirectPath,
+
+ // Tell SS to bubble exceptions up to here
+ WriteErrorsToResponse = false,
+
+ DebugMode = true
+ });
+ }
+
+ container.Register(Kernel);
+
+ foreach (var service in Kernel.RestServices)
+ {
+ service.Configure(this);
+ }
+
+ Plugins.Add(new SwaggerFeature());
+ Plugins.Add(new CorsFeature());
+
+ Serialization.JsonSerializer.Configure();
+
+ 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)
+ {
+ // *** Already running - just leave it in place
+ if (IsStarted)
+ {
+ return;
+ }
+
+ if (Listener == null)
+ {
+ Listener = new HttpListener();
+ }
+
+ EndpointHost.Config.ServiceStackHandlerFactoryPath = HttpListenerRequestWrapper.GetHandlerPathIfAny(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;
+ }
+
+ RaiseReceiveWebRequest(context);
+
+ Task.Run(() =>
+ {
+ 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), Endpoint = ctx.Request.RemoteEndPoint });
+ }
+ }
+ 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 (Kernel.Configuration.EnableHttpLevelLogging)
+ {
+ 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 (Kernel.Configuration.EnableHttpLevelLogging)
+ {
+ 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 = Kernel.RestServices.Select(r => r.GetType()).ToArray();
+
+ return new ServiceManager(new Container(), new ServiceController(() => types));
+ }
+ }
+
+ /// <summary>
+ /// Class WebSocketConnectEventArgs
+ /// </summary>
+ public class WebSocketConnectEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the web socket.
+ /// </summary>
+ /// <value>The web socket.</value>
+ public IWebSocket WebSocket { get; set; }
+ /// <summary>
+ /// Gets or sets the endpoint.
+ /// </summary>
+ /// <value>The endpoint.</value>
+ public IPEndPoint Endpoint { get; set; }
+ }
} \ No newline at end of file
diff --git a/MediaBrowser.Common/Net/IRestfulService.cs b/MediaBrowser.Common/Net/IRestfulService.cs
new file mode 100644
index 0000000000..7fc6bb61ec
--- /dev/null
+++ b/MediaBrowser.Common/Net/IRestfulService.cs
@@ -0,0 +1,14 @@
+using ServiceStack.ServiceHost;
+using ServiceStack.WebHost.Endpoints;
+using System;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Interface IRestfulService
+ /// </summary>
+ public interface IRestfulService : IService, IRequiresRequestContext, IDisposable
+ {
+ void Configure(IAppHost appHost);
+ }
+}
diff --git a/MediaBrowser.Common/Net/IWebSocket.cs b/MediaBrowser.Common/Net/IWebSocket.cs
new file mode 100644
index 0000000000..96299e3b2e
--- /dev/null
+++ b/MediaBrowser.Common/Net/IWebSocket.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Interface IWebSocket
+ /// </summary>
+ public interface IWebSocket : IDisposable
+ {
+ /// <summary>
+ /// Gets or sets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ WebSocketState State { get; }
+
+ /// <summary>
+ /// Gets or sets the receive action.
+ /// </summary>
+ /// <value>The receive action.</value>
+ Action<WebSocketMessageInfo> OnReceiveDelegate { get; set; }
+
+ /// <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>
+ Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Common/Net/MimeTypes.cs b/MediaBrowser.Common/Net/MimeTypes.cs
index fb85b0f2a3..8f980200e7 100644
--- a/MediaBrowser.Common/Net/MimeTypes.cs
+++ b/MediaBrowser.Common/Net/MimeTypes.cs
@@ -1,160 +1,206 @@
-using System;
-using System.IO;
-
-namespace MediaBrowser.Common.Net
-{
- public static class MimeTypes
- {
- public static string JsonMimeType = "application/json";
-
- public static string GetMimeType(string path)
- {
- var ext = Path.GetExtension(path);
-
- // http://en.wikipedia.org/wiki/Internet_media_type
- // Add more as needed
-
- // Type video
- if (ext.EndsWith("mpg", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("mpeg", StringComparison.OrdinalIgnoreCase))
- {
- return "video/mpeg";
- }
- if (ext.EndsWith("mp4", StringComparison.OrdinalIgnoreCase))
- {
- return "video/mp4";
- }
- if (ext.EndsWith("ogv", StringComparison.OrdinalIgnoreCase))
- {
- return "video/ogg";
- }
- if (ext.EndsWith("mov", StringComparison.OrdinalIgnoreCase))
- {
- return "video/quicktime";
- }
- if (ext.EndsWith("webm", StringComparison.OrdinalIgnoreCase))
- {
- return "video/webm";
- }
- if (ext.EndsWith("mkv", StringComparison.OrdinalIgnoreCase))
- {
- return "video/x-matroska";
- }
- if (ext.EndsWith("wmv", StringComparison.OrdinalIgnoreCase))
- {
- return "video/x-ms-wmv";
- }
- if (ext.EndsWith("flv", StringComparison.OrdinalIgnoreCase))
- {
- return "video/x-flv";
- }
- if (ext.EndsWith("avi", StringComparison.OrdinalIgnoreCase))
- {
- return "video/avi";
- }
- if (ext.EndsWith("m4v", StringComparison.OrdinalIgnoreCase))
- {
- return "video/x-m4v";
- }
- if (ext.EndsWith("asf", StringComparison.OrdinalIgnoreCase))
- {
- return "video/x-ms-asf";
- }
- if (ext.EndsWith("3gp", StringComparison.OrdinalIgnoreCase))
- {
- return "video/3gpp";
- }
- if (ext.EndsWith("3g2", StringComparison.OrdinalIgnoreCase))
- {
- return "video/3gpp2";
- }
- if (ext.EndsWith("ts", StringComparison.OrdinalIgnoreCase))
- {
- return "video/mp2t";
- }
-
- // Type text
- if (ext.EndsWith("css", StringComparison.OrdinalIgnoreCase))
- {
- return "text/css";
- }
- if (ext.EndsWith("csv", StringComparison.OrdinalIgnoreCase))
- {
- return "text/csv";
- }
- if (ext.EndsWith("html", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("html", StringComparison.OrdinalIgnoreCase))
- {
- return "text/html";
- }
- if (ext.EndsWith("txt", StringComparison.OrdinalIgnoreCase))
- {
- return "text/plain";
- }
-
- // Type image
- if (ext.EndsWith("gif", StringComparison.OrdinalIgnoreCase))
- {
- return "image/gif";
- }
- if (ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("jpeg", StringComparison.OrdinalIgnoreCase))
- {
- return "image/jpeg";
- }
- if (ext.EndsWith("png", StringComparison.OrdinalIgnoreCase))
- {
- return "image/png";
- }
- if (ext.EndsWith("ico", StringComparison.OrdinalIgnoreCase))
- {
- return "image/vnd.microsoft.icon";
- }
-
- // Type audio
- if (ext.EndsWith("mp3", StringComparison.OrdinalIgnoreCase))
- {
- return "audio/mpeg";
- }
- if (ext.EndsWith("m4a", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("aac", StringComparison.OrdinalIgnoreCase))
- {
- return "audio/mp4";
- }
- if (ext.EndsWith("webma", StringComparison.OrdinalIgnoreCase))
- {
- return "audio/webm";
- }
- if (ext.EndsWith("wav", StringComparison.OrdinalIgnoreCase))
- {
- return "audio/wav";
- }
- if (ext.EndsWith("wma", StringComparison.OrdinalIgnoreCase))
- {
- return "audio/x-ms-wma";
- }
- if (ext.EndsWith("flac", StringComparison.OrdinalIgnoreCase))
- {
- return "audio/flac";
- }
- if (ext.EndsWith("aac", StringComparison.OrdinalIgnoreCase))
- {
- return "audio/x-aac";
- }
- if (ext.EndsWith("ogg", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("oga", StringComparison.OrdinalIgnoreCase))
- {
- return "audio/ogg";
- }
-
- // Playlists
- if (ext.EndsWith("m3u8", StringComparison.OrdinalIgnoreCase))
- {
- return "application/x-mpegURL";
- }
-
- // Misc
- if (ext.EndsWith("dll", StringComparison.OrdinalIgnoreCase))
- {
- return "application/x-msdownload";
- }
-
- throw new InvalidOperationException("Argument not supported: " + path);
- }
- }
-}
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Class MimeTypes
+ /// </summary>
+ public static class MimeTypes
+ {
+ /// <summary>
+ /// The json MIME type
+ /// </summary>
+ public static string JsonMimeType = "application/json";
+
+ /// <summary>
+ /// Gets the type of the MIME.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">path</exception>
+ /// <exception cref="System.InvalidOperationException">Argument not supported: + path</exception>
+ public static string GetMimeType(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var ext = Path.GetExtension(path) ?? string.Empty;
+
+ // http://en.wikipedia.org/wiki/Internet_media_type
+ // Add more as needed
+
+ // Type video
+ if (ext.Equals(".mpg", StringComparison.OrdinalIgnoreCase) || ext.EndsWith("mpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/mpeg";
+ }
+ if (ext.Equals(".mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/mp4";
+ }
+ if (ext.Equals(".ogv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/ogg";
+ }
+ if (ext.Equals(".mov", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/quicktime";
+ }
+ if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/webm";
+ }
+ if (ext.Equals(".mkv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/x-matroska";
+ }
+ if (ext.Equals(".wmv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/x-ms-wmv";
+ }
+ if (ext.Equals(".flv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/x-flv";
+ }
+ if (ext.Equals(".avi", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/avi";
+ }
+ if (ext.Equals(".m4v", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/x-m4v";
+ }
+ if (ext.EndsWith("asf", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/x-ms-asf";
+ }
+ if (ext.Equals(".3gp", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/3gpp";
+ }
+ if (ext.Equals(".3g2", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/3gpp2";
+ }
+ if (ext.Equals(".ts", StringComparison.OrdinalIgnoreCase))
+ {
+ return "video/mp2t";
+ }
+
+ // Type text
+ if (ext.Equals(".css", StringComparison.OrdinalIgnoreCase))
+ {
+ return "text/css";
+ }
+ if (ext.Equals(".csv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "text/csv";
+ }
+ if (ext.Equals(".html", StringComparison.OrdinalIgnoreCase) || ext.Equals(".htm", StringComparison.OrdinalIgnoreCase))
+ {
+ return "text/html; charset=UTF-8";
+ }
+ if (ext.Equals(".txt", StringComparison.OrdinalIgnoreCase))
+ {
+ return "text/plain";
+ }
+ if (ext.Equals(".xml", StringComparison.OrdinalIgnoreCase))
+ {
+ return "application/xml";
+ }
+
+ // Type image
+ if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase))
+ {
+ return "image/gif";
+ }
+ if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return "image/jpeg";
+ }
+ if (ext.Equals(".png", StringComparison.OrdinalIgnoreCase))
+ {
+ return "image/png";
+ }
+ if (ext.Equals(".ico", StringComparison.OrdinalIgnoreCase))
+ {
+ return "image/vnd.microsoft.icon";
+ }
+
+ // Type audio
+ if (ext.Equals(".mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/mpeg";
+ }
+ if (ext.Equals(".m4a", StringComparison.OrdinalIgnoreCase) || ext.Equals(".aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/mp4";
+ }
+ if (ext.Equals(".webma", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/webm";
+ }
+ if (ext.Equals(".wav", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/wav";
+ }
+ if (ext.Equals(".wma", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/x-ms-wma";
+ }
+ if (ext.Equals(".flac", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/flac";
+ }
+ if (ext.Equals(".aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/x-aac";
+ }
+ if (ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".oga", StringComparison.OrdinalIgnoreCase))
+ {
+ return "audio/ogg";
+ }
+
+ // Playlists
+ if (ext.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
+ {
+ return "application/x-mpegURL";
+ }
+
+ // Misc
+ if (ext.Equals(".dll", StringComparison.OrdinalIgnoreCase))
+ {
+ return "application/x-msdownload";
+ }
+
+ // Web
+ if (ext.Equals(".js", StringComparison.OrdinalIgnoreCase))
+ {
+ return "application/x-javascript";
+ }
+
+ if (ext.Equals(".woff", StringComparison.OrdinalIgnoreCase))
+ {
+ return "font/woff";
+ }
+
+ if (ext.Equals(".ttf", StringComparison.OrdinalIgnoreCase))
+ {
+ return "font/ttf";
+ }
+ if (ext.Equals(".eot", StringComparison.OrdinalIgnoreCase))
+ {
+ return "application/vnd.ms-fontobject";
+ }
+ if (ext.Equals(".svg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".svgz", StringComparison.OrdinalIgnoreCase))
+ {
+ return "image/svg+xml";
+ }
+
+ throw new InvalidOperationException("Argument not supported: " + path);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Net/NativeWebSocket.cs b/MediaBrowser.Common/Net/NativeWebSocket.cs
new file mode 100644
index 0000000000..d57deca54e
--- /dev/null
+++ b/MediaBrowser.Common/Net/NativeWebSocket.cs
@@ -0,0 +1,153 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Logging;
+using System;
+using System.IO;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Class NativeWebSocket
+ /// </summary>
+ public class NativeWebSocket : IWebSocket
+ {
+ /// <summary>
+ /// The logger
+ /// </summary>
+ private static ILogger Logger = LogManager.GetLogger("NativeWebSocket");
+
+ /// <summary>
+ /// Gets or sets the web socket.
+ /// </summary>
+ /// <value>The web socket.</value>
+ private WebSocket WebSocket { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NativeWebSocket" /> class.
+ /// </summary>
+ /// <param name="socket">The socket.</param>
+ /// <exception cref="System.ArgumentNullException">socket</exception>
+ public NativeWebSocket(WebSocket socket)
+ {
+ if (socket == null)
+ {
+ throw new ArgumentNullException("socket");
+ }
+
+ WebSocket = socket;
+
+ Receive();
+ }
+
+ /// <summary>
+ /// Gets or sets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ public WebSocketState State
+ {
+ get { return WebSocket.State; }
+ }
+
+ /// <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)
+ {
+ using (var memoryStream = new MemoryStream(bytes))
+ {
+ try
+ {
+ var messageResult = JsonSerializer.DeserializeFromStream<WebSocketMessageInfo>(memoryStream);
+
+ OnReceiveDelegate(messageResult);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error processing web socket message", ex);
+ }
+ }
+ }
+ }
+ }
+
+ /// <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)
+ {
+ return WebSocket.SendAsync(new ArraySegment<byte>(bytes), type, 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<WebSocketMessageInfo> OnReceiveDelegate { get; set; }
+ }
+}
diff --git a/MediaBrowser.Common/Net/NetUtils.cs b/MediaBrowser.Common/Net/NetUtils.cs
new file mode 100644
index 0000000000..eb50a879d6
--- /dev/null
+++ b/MediaBrowser.Common/Net/NetUtils.cs
@@ -0,0 +1,219 @@
+using MediaBrowser.Common.Win32;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Management;
+using System.Net;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Class NetUtils
+ /// </summary>
+ public static class NetUtils
+ {
+ /// <summary>
+ /// Gets the machine's local ip address
+ /// </summary>
+ /// <returns>IPAddress.</returns>
+ public static IPAddress GetLocalIpAddress()
+ {
+ var host = Dns.GetHostEntry(Dns.GetHostName());
+
+ return host.AddressList.FirstOrDefault(i => i.AddressFamily == AddressFamily.InterNetwork);
+ }
+
+ /// <summary>
+ /// Gets a random port number that is currently available
+ /// </summary>
+ /// <returns>System.Int32.</returns>
+ public static int GetRandomUnusedPort()
+ {
+ var listener = new TcpListener(IPAddress.Any, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+ return port;
+ }
+
+ /// <summary>
+ /// Creates the netsh URL registration.
+ /// </summary>
+ /// <param name="urlPrefix">The URL prefix.</param>
+ public static void CreateNetshUrlRegistration(string urlPrefix)
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "netsh",
+ Arguments = string.Format("http add urlacl url={0} user=\"NT AUTHORITY\\Authenticated Users\"", urlPrefix),
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ Verb = "runas",
+ ErrorDialog = false
+ };
+
+ using (var process = Process.Start(startInfo))
+ {
+ process.WaitForExit();
+ }
+ }
+
+ /// <summary>
+ /// Adds the windows firewall rule.
+ /// </summary>
+ /// <param name="port">The port.</param>
+ /// <param name="protocol">The protocol.</param>
+ public static void AddWindowsFirewallRule(int port, NetworkProtocol protocol)
+ {
+ // First try to remove it so we don't end up creating duplicates
+ RemoveWindowsFirewallRule(port, protocol);
+
+ var args = string.Format("advfirewall firewall add rule name=\"Port {0}\" dir=in action=allow protocol={1} localport={0}", port, protocol);
+
+ RunNetsh(args);
+ }
+
+ /// <summary>
+ /// Removes the windows firewall rule.
+ /// </summary>
+ /// <param name="port">The port.</param>
+ /// <param name="protocol">The protocol.</param>
+ public static void RemoveWindowsFirewallRule(int port, NetworkProtocol protocol)
+ {
+ var args = string.Format("advfirewall firewall delete rule name=\"Port {0}\" protocol={1} localport={0}", port, protocol);
+
+ RunNetsh(args);
+ }
+
+ /// <summary>
+ /// Runs the netsh.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ private static void RunNetsh(string args)
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "netsh",
+ Arguments = args,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ Verb = "runas",
+ ErrorDialog = false
+ };
+
+ using (var process = new Process { StartInfo = startInfo })
+ {
+ process.Start();
+ process.WaitForExit();
+ }
+ }
+
+ /// <summary>
+ /// Returns MAC Address from first Network Card in Computer
+ /// </summary>
+ /// <returns>[string] MAC Address</returns>
+ public static string GetMacAddress()
+ {
+ var mc = new ManagementClass("Win32_NetworkAdapterConfiguration");
+ var moc = mc.GetInstances();
+ var macAddress = String.Empty;
+ foreach (ManagementObject mo in moc)
+ {
+ if (macAddress == String.Empty) // only return MAC Address from first card
+ {
+ try
+ {
+ if ((bool)mo["IPEnabled"]) macAddress = mo["MacAddress"].ToString();
+ }
+ catch
+ {
+ mo.Dispose();
+ return "";
+ }
+ }
+ mo.Dispose();
+ }
+
+ return macAddress.Replace(":", "");
+ }
+
+ /// <summary>
+ /// Uses the DllImport : NetServerEnum with all its required parameters
+ /// (see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/netmgmt/netmgmt/netserverenum.asp
+ /// for full details or method signature) to retrieve a list of domain SV_TYPE_WORKSTATION
+ /// and SV_TYPE_SERVER PC's
+ /// </summary>
+ /// <returns>Arraylist that represents all the SV_TYPE_WORKSTATION and SV_TYPE_SERVER
+ /// PC's in the Domain</returns>
+ public static IEnumerable<string> GetNetworkComputers()
+ {
+ //local fields
+ const int MAX_PREFERRED_LENGTH = -1;
+ var SV_TYPE_WORKSTATION = 1;
+ var SV_TYPE_SERVER = 2;
+ var buffer = IntPtr.Zero;
+ var tmpBuffer = IntPtr.Zero;
+ var entriesRead = 0;
+ var totalEntries = 0;
+ var resHandle = 0;
+ var sizeofINFO = Marshal.SizeOf(typeof(_SERVER_INFO_100));
+
+ try
+ {
+ //call the DllImport : NetServerEnum with all its required parameters
+ //see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/netmgmt/netmgmt/netserverenum.asp
+ //for full details of method signature
+ var ret = NativeMethods.NetServerEnum(null, 100, ref buffer, MAX_PREFERRED_LENGTH, out entriesRead, out totalEntries, SV_TYPE_WORKSTATION | SV_TYPE_SERVER, null, out resHandle);
+
+ //if the returned with a NERR_Success (C++ term), =0 for C#
+ if (ret == 0)
+ {
+ //loop through all SV_TYPE_WORKSTATION and SV_TYPE_SERVER PC's
+ for (var i = 0; i < totalEntries; i++)
+ {
+ //get pointer to, Pointer to the buffer that received the data from
+ //the call to NetServerEnum. Must ensure to use correct size of
+ //STRUCTURE to ensure correct location in memory is pointed to
+ tmpBuffer = new IntPtr((int)buffer + (i * sizeofINFO));
+ //Have now got a pointer to the list of SV_TYPE_WORKSTATION and
+ //SV_TYPE_SERVER PC's, which is unmanaged memory
+ //Needs to Marshal data from an unmanaged block of memory to a
+ //managed object, again using STRUCTURE to ensure the correct data
+ //is marshalled
+ var svrInfo = (_SERVER_INFO_100)Marshal.PtrToStructure(tmpBuffer, typeof(_SERVER_INFO_100));
+
+ //add the PC names to the ArrayList
+ if (!string.IsNullOrEmpty(svrInfo.sv100_name))
+ {
+ yield return svrInfo.sv100_name;
+ }
+ }
+ }
+ }
+ finally
+ {
+ //The NetApiBufferFree function frees
+ //the memory that the NetApiBufferAllocate function allocates
+ NativeMethods.NetApiBufferFree(buffer);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Enum NetworkProtocol
+ /// </summary>
+ public enum NetworkProtocol
+ {
+ /// <summary>
+ /// The TCP
+ /// </summary>
+ Tcp,
+ /// <summary>
+ /// The UDP
+ /// </summary>
+ Udp
+ }
+}
diff --git a/MediaBrowser.Common/Net/NetworkShares.cs b/MediaBrowser.Common/Net/NetworkShares.cs
new file mode 100644
index 0000000000..202865b4c1
--- /dev/null
+++ b/MediaBrowser.Common/Net/NetworkShares.cs
@@ -0,0 +1,644 @@
+using System;
+using System.IO;
+using System.Collections;
+using System.Runtime.InteropServices;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Type of share
+ /// </summary>
+ [Flags]
+ public enum ShareType
+ {
+ /// <summary>Disk share</summary>
+ Disk = 0,
+ /// <summary>Printer share</summary>
+ Printer = 1,
+ /// <summary>Device share</summary>
+ Device = 2,
+ /// <summary>IPC share</summary>
+ IPC = 3,
+ /// <summary>Special share</summary>
+ Special = -2147483648, // 0x80000000,
+ }
+
+ #region Share
+
+ /// <summary>
+ /// Information about a local share
+ /// </summary>
+ public class Share
+ {
+ #region Private data
+
+ private string _server;
+ private string _netName;
+ private string _path;
+ private ShareType _shareType;
+ private string _remark;
+
+ #endregion
+
+ #region Constructor
+
+ /// <summary>
+ /// Constructor
+ /// </summary>
+ /// <param name="Server"></param>
+ /// <param name="shi"></param>
+ public Share(string server, string netName, string path, ShareType shareType, string remark)
+ {
+ if (ShareType.Special == shareType && "IPC$" == netName)
+ {
+ shareType |= ShareType.IPC;
+ }
+
+ _server = server;
+ _netName = netName;
+ _path = path;
+ _shareType = shareType;
+ _remark = remark;
+ }
+
+ #endregion
+
+ #region Properties
+
+ /// <summary>
+ /// The name of the computer that this share belongs to
+ /// </summary>
+ public string Server
+ {
+ get { return _server; }
+ }
+
+ /// <summary>
+ /// Share name
+ /// </summary>
+ public string NetName
+ {
+ get { return _netName; }
+ }
+
+ /// <summary>
+ /// Local path
+ /// </summary>
+ public string Path
+ {
+ get { return _path; }
+ }
+
+ /// <summary>
+ /// Share type
+ /// </summary>
+ public ShareType ShareType
+ {
+ get { return _shareType; }
+ }
+
+ /// <summary>
+ /// Comment
+ /// </summary>
+ public string Remark
+ {
+ get { return _remark; }
+ }
+
+ /// <summary>
+ /// Returns true if this is a file system share
+ /// </summary>
+ public bool IsFileSystem
+ {
+ get
+ {
+ // Shared device
+ if (0 != (_shareType & ShareType.Device)) return false;
+ // IPC share
+ if (0 != (_shareType & ShareType.IPC)) return false;
+ // Shared printer
+ if (0 != (_shareType & ShareType.Printer)) return false;
+
+ // Standard disk share
+ if (0 == (_shareType & ShareType.Special)) return true;
+
+ // Special disk share (e.g. C$)
+ if (ShareType.Special == _shareType && null != _netName && 0 != _netName.Length)
+ return true;
+ else
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Get the root of a disk-based share
+ /// </summary>
+ public DirectoryInfo Root
+ {
+ get
+ {
+ if (IsFileSystem)
+ {
+ if (null == _server || 0 == _server.Length)
+ if (null == _path || 0 == _path.Length)
+ return new DirectoryInfo(ToString());
+ else
+ return new DirectoryInfo(_path);
+ else
+ return new DirectoryInfo(ToString());
+ }
+ else
+ return null;
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Returns the path to this share
+ /// </summary>
+ /// <returns></returns>
+ public override string ToString()
+ {
+ if (null == _server || 0 == _server.Length)
+ {
+ return string.Format(@"\\{0}\{1}", Environment.MachineName, _netName);
+ }
+ else
+ return string.Format(@"\\{0}\{1}", _server, _netName);
+ }
+
+ /// <summary>
+ /// Returns true if this share matches the local path
+ /// </summary>
+ /// <param name="path"></param>
+ /// <returns></returns>
+ public bool MatchesPath(string path)
+ {
+ if (!IsFileSystem) return false;
+ if (null == path || 0 == path.Length) return true;
+
+ return path.ToLower().StartsWith(_path.ToLower());
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// A collection of shares
+ /// </summary>
+ public class ShareCollection : ReadOnlyCollectionBase
+ {
+ #region Platform
+
+ /// <summary>
+ /// Is this an NT platform?
+ /// </summary>
+ protected static bool IsNT
+ {
+ get { return (PlatformID.Win32NT == Environment.OSVersion.Platform); }
+ }
+
+ /// <summary>
+ /// Returns true if this is Windows 2000 or higher
+ /// </summary>
+ protected static bool IsW2KUp
+ {
+ get
+ {
+ OperatingSystem os = Environment.OSVersion;
+ if (PlatformID.Win32NT == os.Platform && os.Version.Major >= 5)
+ return true;
+ else
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Interop
+
+ #region Constants
+
+ /// <summary>Maximum path length</summary>
+ protected const int MAX_PATH = 260;
+ /// <summary>No error</summary>
+ protected const int NO_ERROR = 0;
+ /// <summary>Access denied</summary>
+ protected const int ERROR_ACCESS_DENIED = 5;
+ /// <summary>Access denied</summary>
+ protected const int ERROR_WRONG_LEVEL = 124;
+ /// <summary>More data available</summary>
+ protected const int ERROR_MORE_DATA = 234;
+ /// <summary>Not connected</summary>
+ protected const int ERROR_NOT_CONNECTED = 2250;
+ /// <summary>Level 1</summary>
+ protected const int UNIVERSAL_NAME_INFO_LEVEL = 1;
+ /// <summary>Max extries (9x)</summary>
+ protected const int MAX_SI50_ENTRIES = 20;
+
+ #endregion
+
+ #region Structures
+
+ /// <summary>Unc name</summary>
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+ protected struct UNIVERSAL_NAME_INFO
+ {
+ [MarshalAs(UnmanagedType.LPTStr)]
+ public string lpUniversalName;
+ }
+
+ /// <summary>Share information, NT, level 2</summary>
+ /// <remarks>
+ /// Requires admin rights to work.
+ /// </remarks>
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ protected struct SHARE_INFO_2
+ {
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string NetName;
+ public ShareType ShareType;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string Remark;
+ public int Permissions;
+ public int MaxUsers;
+ public int CurrentUsers;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string Path;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string Password;
+ }
+
+ /// <summary>Share information, NT, level 1</summary>
+ /// <remarks>
+ /// Fallback when no admin rights.
+ /// </remarks>
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ protected struct SHARE_INFO_1
+ {
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string NetName;
+ public ShareType ShareType;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string Remark;
+ }
+
+ /// <summary>Share information, Win9x</summary>
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
+ protected struct SHARE_INFO_50
+ {
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 13)]
+ public string NetName;
+
+ public byte bShareType;
+ public ushort Flags;
+
+ [MarshalAs(UnmanagedType.LPTStr)]
+ public string Remark;
+ [MarshalAs(UnmanagedType.LPTStr)]
+ public string Path;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 9)]
+ public string PasswordRW;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 9)]
+ public string PasswordRO;
+
+ public ShareType ShareType
+ {
+ get { return (ShareType)((int)bShareType & 0x7F); }
+ }
+ }
+
+ /// <summary>Share information level 1, Win9x</summary>
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
+ protected struct SHARE_INFO_1_9x
+ {
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 13)]
+ public string NetName;
+ public byte Padding;
+
+ public ushort bShareType;
+
+ [MarshalAs(UnmanagedType.LPTStr)]
+ public string Remark;
+
+ public ShareType ShareType
+ {
+ get { return (ShareType)((int)bShareType & 0x7FFF); }
+ }
+ }
+
+ #endregion
+
+ #region Functions
+
+ /// <summary>Get a UNC name</summary>
+ [DllImport("mpr", CharSet = CharSet.Auto)]
+ protected static extern int WNetGetUniversalName(string lpLocalPath,
+ int dwInfoLevel, ref UNIVERSAL_NAME_INFO lpBuffer, ref int lpBufferSize);
+
+ /// <summary>Get a UNC name</summary>
+ [DllImport("mpr", CharSet = CharSet.Auto)]
+ protected static extern int WNetGetUniversalName(string lpLocalPath,
+ int dwInfoLevel, IntPtr lpBuffer, ref int lpBufferSize);
+
+ /// <summary>Enumerate shares (NT)</summary>
+ [DllImport("netapi32", CharSet = CharSet.Unicode)]
+ protected static extern int NetShareEnum(string lpServerName, int dwLevel,
+ out IntPtr lpBuffer, int dwPrefMaxLen, out int entriesRead,
+ out int totalEntries, ref int hResume);
+
+ /// <summary>Enumerate shares (9x)</summary>
+ [DllImport("svrapi", CharSet = CharSet.Ansi)]
+ protected static extern int NetShareEnum(
+ [MarshalAs(UnmanagedType.LPTStr)] string lpServerName, int dwLevel,
+ IntPtr lpBuffer, ushort cbBuffer, out ushort entriesRead,
+ out ushort totalEntries);
+
+ /// <summary>Free the buffer (NT)</summary>
+ [DllImport("netapi32")]
+ protected static extern int NetApiBufferFree(IntPtr lpBuffer);
+
+ #endregion
+
+ #region Enumerate shares
+
+ /// <summary>
+ /// Enumerates the shares on Windows NT
+ /// </summary>
+ /// <param name="server">The server name</param>
+ /// <param name="shares">The ShareCollection</param>
+ protected static void EnumerateSharesNT(string server, ShareCollection shares)
+ {
+ int level = 2;
+ int entriesRead, totalEntries, nRet, hResume = 0;
+ IntPtr pBuffer = IntPtr.Zero;
+
+ try
+ {
+ nRet = NetShareEnum(server, level, out pBuffer, -1,
+ out entriesRead, out totalEntries, ref hResume);
+
+ if (ERROR_ACCESS_DENIED == nRet)
+ {
+ //Need admin for level 2, drop to level 1
+ level = 1;
+ nRet = NetShareEnum(server, level, out pBuffer, -1,
+ out entriesRead, out totalEntries, ref hResume);
+ }
+
+ if (NO_ERROR == nRet && entriesRead > 0)
+ {
+ Type t = (2 == level) ? typeof(SHARE_INFO_2) : typeof(SHARE_INFO_1);
+ int offset = Marshal.SizeOf(t);
+
+ for (int i = 0, lpItem = pBuffer.ToInt32(); i < entriesRead; i++, lpItem += offset)
+ {
+ IntPtr pItem = new IntPtr(lpItem);
+ if (1 == level)
+ {
+ SHARE_INFO_1 si = (SHARE_INFO_1)Marshal.PtrToStructure(pItem, t);
+ shares.Add(si.NetName, string.Empty, si.ShareType, si.Remark);
+ }
+ else
+ {
+ SHARE_INFO_2 si = (SHARE_INFO_2)Marshal.PtrToStructure(pItem, t);
+ shares.Add(si.NetName, si.Path, si.ShareType, si.Remark);
+ }
+ }
+ }
+
+ }
+ finally
+ {
+ // Clean up buffer allocated by system
+ if (IntPtr.Zero != pBuffer)
+ NetApiBufferFree(pBuffer);
+ }
+ }
+
+ /// <summary>
+ /// Enumerates the shares on Windows 9x
+ /// </summary>
+ /// <param name="server">The server name</param>
+ /// <param name="shares">The ShareCollection</param>
+ protected static void EnumerateShares9x(string server, ShareCollection shares)
+ {
+ int level = 50;
+ int nRet = 0;
+ ushort entriesRead, totalEntries;
+
+ Type t = typeof(SHARE_INFO_50);
+ int size = Marshal.SizeOf(t);
+ ushort cbBuffer = (ushort)(MAX_SI50_ENTRIES * size);
+ //On Win9x, must allocate buffer before calling API
+ IntPtr pBuffer = Marshal.AllocHGlobal(cbBuffer);
+
+ try
+ {
+ nRet = NetShareEnum(server, level, pBuffer, cbBuffer,
+ out entriesRead, out totalEntries);
+
+ if (ERROR_WRONG_LEVEL == nRet)
+ {
+ level = 1;
+ t = typeof(SHARE_INFO_1_9x);
+ size = Marshal.SizeOf(t);
+
+ nRet = NetShareEnum(server, level, pBuffer, cbBuffer,
+ out entriesRead, out totalEntries);
+ }
+
+ if (NO_ERROR == nRet || ERROR_MORE_DATA == nRet)
+ {
+ for (int i = 0, lpItem = pBuffer.ToInt32(); i < entriesRead; i++, lpItem += size)
+ {
+ IntPtr pItem = new IntPtr(lpItem);
+
+ if (1 == level)
+ {
+ SHARE_INFO_1_9x si = (SHARE_INFO_1_9x)Marshal.PtrToStructure(pItem, t);
+ shares.Add(si.NetName, string.Empty, si.ShareType, si.Remark);
+ }
+ else
+ {
+ SHARE_INFO_50 si = (SHARE_INFO_50)Marshal.PtrToStructure(pItem, t);
+ shares.Add(si.NetName, si.Path, si.ShareType, si.Remark);
+ }
+ }
+ }
+ else
+ Console.WriteLine(nRet);
+
+ }
+ finally
+ {
+ //Clean up buffer
+ Marshal.FreeHGlobal(pBuffer);
+ }
+ }
+
+ /// <summary>
+ /// Enumerates the shares
+ /// </summary>
+ /// <param name="server">The server name</param>
+ /// <param name="shares">The ShareCollection</param>
+ protected static void EnumerateShares(string server, ShareCollection shares)
+ {
+ if (null != server && 0 != server.Length && !IsW2KUp)
+ {
+ server = server.ToUpper();
+
+ // On NT4, 9x and Me, server has to start with "\\"
+ if (!('\\' == server[0] && '\\' == server[1]))
+ server = @"\\" + server;
+ }
+
+ if (IsNT)
+ EnumerateSharesNT(server, shares);
+ else
+ EnumerateShares9x(server, shares);
+ }
+
+ #endregion
+
+ #endregion
+
+ #region Static methods
+
+ /// <summary>
+ /// Returns true if fileName is a valid local file-name of the form:
+ /// X:\, where X is a drive letter from A-Z
+ /// </summary>
+ /// <param name="fileName">The filename to check</param>
+ /// <returns></returns>
+ public static bool IsValidFilePath(string fileName)
+ {
+ if (null == fileName || 0 == fileName.Length) return false;
+
+ char drive = char.ToUpper(fileName[0]);
+ if ('A' > drive || drive > 'Z')
+ return false;
+
+ else if (Path.VolumeSeparatorChar != fileName[1])
+ return false;
+ else if (Path.DirectorySeparatorChar != fileName[2])
+ return false;
+ else
+ return true;
+ }
+
+ #endregion
+
+ /// <summary>The name of the server this collection represents</summary>
+ private string _server;
+
+ #region Constructor
+
+ /// <summary>
+ /// Default constructor - local machine
+ /// </summary>
+ public ShareCollection()
+ {
+ _server = string.Empty;
+ EnumerateShares(_server, this);
+ }
+
+ /// <summary>
+ /// Constructor
+ /// </summary>
+ /// <param name="Server"></param>
+ public ShareCollection(string server)
+ {
+ _server = server;
+ EnumerateShares(_server, this);
+ }
+
+ #endregion
+
+ #region Add
+
+ protected void Add(Share share)
+ {
+ InnerList.Add(share);
+ }
+
+ protected void Add(string netName, string path, ShareType shareType, string remark)
+ {
+ InnerList.Add(new Share(_server, netName, path, shareType, remark));
+ }
+
+ #endregion
+
+ #region Properties
+
+ /// <summary>
+ /// Returns the name of the server this collection represents
+ /// </summary>
+ public string Server
+ {
+ get { return _server; }
+ }
+
+ /// <summary>
+ /// Returns the <see cref="Share"/> at the specified index.
+ /// </summary>
+ public Share this[int index]
+ {
+ get { return (Share)InnerList[index]; }
+ }
+
+ /// <summary>
+ /// Returns the <see cref="Share"/> which matches a given local path
+ /// </summary>
+ /// <param name="path">The path to match</param>
+ public Share this[string path]
+ {
+ get
+ {
+ if (null == path || 0 == path.Length) return null;
+
+ path = Path.GetFullPath(path);
+ if (!IsValidFilePath(path)) return null;
+
+ Share match = null;
+
+ for (int i = 0; i < InnerList.Count; i++)
+ {
+ Share s = (Share)InnerList[i];
+
+ if (s.IsFileSystem && s.MatchesPath(path))
+ {
+ //Store first match
+ if (null == match)
+ match = s;
+
+ // If this has a longer path,
+ // and this is a disk share or match is a special share,
+ // then this is a better match
+ else if (match.Path.Length < s.Path.Length)
+ {
+ if (ShareType.Disk == s.ShareType || ShareType.Disk != match.ShareType)
+ match = s;
+ }
+ }
+ }
+
+ return match;
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Copy this collection to an array
+ /// </summary>
+ /// <param name="array"></param>
+ /// <param name="index"></param>
+ public void CopyTo(Share[] array, int index)
+ {
+ InnerList.CopyTo(array, index);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Net/Request.cs b/MediaBrowser.Common/Net/Request.cs
deleted file mode 100644
index 795c9c36ba..0000000000
--- a/MediaBrowser.Common/Net/Request.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-
-namespace MediaBrowser.Common.Net
-{
- public class Request
- {
- public string HttpMethod { get; set; }
- public IDictionary<string, IEnumerable<string>> Headers { get; set; }
- public Stream InputStream { get; set; }
- public string RawUrl { get; set; }
- public int ContentLength
- {
- get { return int.Parse(Headers["Content-Length"].First()); }
- }
- }
-} \ No newline at end of file
diff --git a/MediaBrowser.Common/Net/StaticResult.cs b/MediaBrowser.Common/Net/StaticResult.cs
new file mode 100644
index 0000000000..0dd6372cfa
--- /dev/null
+++ b/MediaBrowser.Common/Net/StaticResult.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ public class StaticResult
+ {
+ public Stream Stream { get; set; }
+ }
+}
diff --git a/MediaBrowser.Common/Net/UdpServer.cs b/MediaBrowser.Common/Net/UdpServer.cs
new file mode 100644
index 0000000000..a3c6a8a78c
--- /dev/null
+++ b/MediaBrowser.Common/Net/UdpServer.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Reactive.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Provides a Udp Server
+ /// </summary>
+ public class UdpServer : IObservable<UdpReceiveResult>, IDisposable
+ {
+ /// <summary>
+ /// The _udp client
+ /// </summary>
+ private readonly UdpClient _udpClient;
+ /// <summary>
+ /// The _stream
+ /// </summary>
+ private readonly IObservable<UdpReceiveResult> _stream;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UdpServer" /> class.
+ /// </summary>
+ /// <param name="endPoint">The end point.</param>
+ /// <exception cref="System.ArgumentNullException">endPoint</exception>
+ public UdpServer(IPEndPoint endPoint)
+ {
+ if (endPoint == null)
+ {
+ throw new ArgumentNullException("endPoint");
+ }
+
+ _udpClient = new UdpClient(endPoint);
+
+ _udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+ //_udpClient.ExclusiveAddressUse = false;
+
+ _stream = CreateObservable();
+ }
+
+ /// <summary>
+ /// Creates the observable.
+ /// </summary>
+ /// <returns>IObservable{UdpReceiveResult}.</returns>
+ private IObservable<UdpReceiveResult> CreateObservable()
+ {
+ return Observable.Create<UdpReceiveResult>(obs =>
+ Observable.FromAsync(() => _udpClient.ReceiveAsync())
+ .Subscribe(obs))
+ .Repeat()
+ .Retry()
+ .Publish()
+ .RefCount();
+ }
+
+ /// <summary>
+ /// Subscribes the specified observer.
+ /// </summary>
+ /// <param name="observer">The observer.</param>
+ /// <returns>IDisposable.</returns>
+ /// <exception cref="System.ArgumentNullException">observer</exception>
+ public IDisposable Subscribe(IObserver<UdpReceiveResult> observer)
+ {
+ if (observer == null)
+ {
+ throw new ArgumentNullException("observer");
+ }
+
+ return _stream.Subscribe(observer);
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <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)
+ {
+ _udpClient.Close();
+ }
+ }
+
+ /// <summary>
+ /// Sends the async.
+ /// </summary>
+ /// <param name="data">The data.</param>
+ /// <param name="endPoint">The end point.</param>
+ /// <returns>Task{System.Int32}.</returns>
+ /// <exception cref="System.ArgumentNullException">data</exception>
+ public async Task<int> SendAsync(string data, IPEndPoint endPoint)
+ {
+ if (data == null)
+ {
+ throw new ArgumentNullException("data");
+ }
+
+ if (endPoint == null)
+ {
+ throw new ArgumentNullException("endPoint");
+ }
+
+ var bytes = Encoding.UTF8.GetBytes(data);
+
+ return await _udpClient.SendAsync(bytes, bytes.Length, endPoint).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Sends the async.
+ /// </summary>
+ /// <param name="bytes">The bytes.</param>
+ /// <param name="endPoint">The end point.</param>
+ /// <returns>Task{System.Int32}.</returns>
+ /// <exception cref="System.ArgumentNullException">bytes</exception>
+ public async Task<int> SendAsync(byte[] bytes, IPEndPoint endPoint)
+ {
+ if (bytes == null)
+ {
+ throw new ArgumentNullException("bytes");
+ }
+
+ if (endPoint == null)
+ {
+ throw new ArgumentNullException("endPoint");
+ }
+
+ return await _udpClient.SendAsync(bytes, bytes.Length, endPoint).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Net/WebSocketConnection.cs b/MediaBrowser.Common/Net/WebSocketConnection.cs
new file mode 100644
index 0000000000..ca12d07be8
--- /dev/null
+++ b/MediaBrowser.Common/Net/WebSocketConnection.cs
@@ -0,0 +1,228 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Net;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Class WebSocketConnection
+ /// </summary>
+ public class WebSocketConnection : IDisposable
+ {
+ /// <summary>
+ /// The _socket
+ /// </summary>
+ private readonly IWebSocket _socket;
+
+ /// <summary>
+ /// The _remote end point
+ /// </summary>
+ public readonly EndPoint RemoteEndPoint;
+
+ /// <summary>
+ /// The _cancellation token source
+ /// </summary>
+ private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+
+ /// <summary>
+ /// The _send semaphore
+ /// </summary>
+ private readonly SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1,1);
+
+ /// <summary>
+ /// The logger
+ /// </summary>
+ private static readonly ILogger Logger = LogManager.GetLogger("WebSocketConnection");
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
+ /// </summary>
+ /// <param name="socket">The socket.</param>
+ /// <param name="remoteEndPoint">The remote end point.</param>
+ /// <param name="receiveAction">The receive action.</param>
+ /// <exception cref="System.ArgumentNullException">socket</exception>
+ public WebSocketConnection(IWebSocket socket, EndPoint remoteEndPoint, Action<WebSocketMessageInfo> receiveAction)
+ {
+ if (socket == null)
+ {
+ throw new ArgumentNullException("socket");
+ }
+ if (remoteEndPoint == null)
+ {
+ throw new ArgumentNullException("remoteEndPoint");
+ }
+ if (receiveAction == null)
+ {
+ throw new ArgumentNullException("receiveAction");
+ }
+
+ _socket = socket;
+ _socket.OnReceiveDelegate = info => OnReceive(info, receiveAction);
+ RemoteEndPoint = remoteEndPoint;
+ }
+
+ /// <summary>
+ /// Called when [receive].
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <param name="callback">The callback.</param>
+ private void OnReceive(WebSocketMessageInfo info, Action<WebSocketMessageInfo> callback)
+ {
+ try
+ {
+ info.Connection = this;
+
+ callback(info);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error processing web socket message", ex);
+ }
+ }
+
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="message">The message.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">message</exception>
+ public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException("message");
+ }
+
+ var bytes = JsonSerializer.SerializeToBytes(message);
+
+ return SendAsync(bytes, cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
+ /// <param name="buffer">The buffer.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
+ {
+ return SendAsync(buffer, WebSocketMessageType.Text, cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
+ /// <param name="buffer">The buffer.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">buffer</exception>
+ public async Task SendAsync(byte[] buffer, WebSocketMessageType type, CancellationToken cancellationToken)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Per msdn docs, attempting to send simultaneous messages will result in one failing.
+ // This should help us workaround that and ensure all messages get sent
+ await _sendSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ await _socket.SendAsync(buffer, type, true, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ Logger.Info("WebSocket message to {0} was cancelled", RemoteEndPoint);
+
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error sending WebSocket message {0}", ex, RemoteEndPoint);
+
+ throw;
+ }
+ finally
+ {
+ _sendSemaphore.Release();
+ }
+ }
+
+ /// <summary>
+ /// Gets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ public WebSocketState State
+ {
+ get { return _socket.State; }
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <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)
+ {
+ _cancellationTokenSource.Dispose();
+ _socket.Dispose();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Class WebSocketMessage
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public class WebSocketMessage<T>
+ {
+ /// <summary>
+ /// Gets or sets the type of the message.
+ /// </summary>
+ /// <value>The type of the message.</value>
+ public string MessageType { get; set; }
+ /// <summary>
+ /// Gets or sets the data.
+ /// </summary>
+ /// <value>The data.</value>
+ public T Data { get; set; }
+ }
+
+ /// <summary>
+ /// Class WebSocketMessageInfo
+ /// </summary>
+ public class WebSocketMessageInfo : WebSocketMessage<string>
+ {
+ /// <summary>
+ /// Gets or sets the connection.
+ /// </summary>
+ /// <value>The connection.</value>
+ public WebSocketConnection Connection { get; set; }
+ }
+}