From 767cdc1f6f6a63ce997fc9476911e2c361f9d402 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Wed, 20 Feb 2013 20:33:05 -0500 Subject: Pushing missing changes --- MediaBrowser.Common/Net/AlchemyWebSocket.cs | 131 ++ MediaBrowser.Common/Net/BaseRestService.cs | 451 +++++++ .../Net/Handlers/BaseActionHandler.cs | 31 + .../Net/Handlers/BaseEmbeddedResourceHandler.cs | 23 - MediaBrowser.Common/Net/Handlers/BaseHandler.cs | 1254 +++++++++++++------- .../Net/Handlers/BaseSerializationHandler.cs | 224 ++-- .../Net/Handlers/IHttpServerHandler.cs | 32 + .../Net/Handlers/StaticFileHandler.cs | 513 ++++---- MediaBrowser.Common/Net/HttpManager.cs | 452 +++++++ MediaBrowser.Common/Net/HttpServer.cs | 482 +++++++- MediaBrowser.Common/Net/IRestfulService.cs | 14 + MediaBrowser.Common/Net/IWebSocket.cs | 35 + MediaBrowser.Common/Net/MimeTypes.cs | 366 +++--- MediaBrowser.Common/Net/NativeWebSocket.cs | 153 +++ MediaBrowser.Common/Net/NetUtils.cs | 219 ++++ MediaBrowser.Common/Net/NetworkShares.cs | 644 ++++++++++ MediaBrowser.Common/Net/Request.cs | 18 - MediaBrowser.Common/Net/StaticResult.cs | 14 + MediaBrowser.Common/Net/UdpServer.cs | 142 +++ MediaBrowser.Common/Net/WebSocketConnection.cs | 228 ++++ 20 files changed, 4418 insertions(+), 1008 deletions(-) create mode 100644 MediaBrowser.Common/Net/AlchemyWebSocket.cs create mode 100644 MediaBrowser.Common/Net/BaseRestService.cs create mode 100644 MediaBrowser.Common/Net/Handlers/BaseActionHandler.cs delete mode 100644 MediaBrowser.Common/Net/Handlers/BaseEmbeddedResourceHandler.cs create mode 100644 MediaBrowser.Common/Net/Handlers/IHttpServerHandler.cs create mode 100644 MediaBrowser.Common/Net/HttpManager.cs create mode 100644 MediaBrowser.Common/Net/IRestfulService.cs create mode 100644 MediaBrowser.Common/Net/IWebSocket.cs create mode 100644 MediaBrowser.Common/Net/NativeWebSocket.cs create mode 100644 MediaBrowser.Common/Net/NetUtils.cs create mode 100644 MediaBrowser.Common/Net/NetworkShares.cs delete mode 100644 MediaBrowser.Common/Net/Request.cs create mode 100644 MediaBrowser.Common/Net/StaticResult.cs create mode 100644 MediaBrowser.Common/Net/UdpServer.cs create mode 100644 MediaBrowser.Common/Net/WebSocketConnection.cs (limited to 'MediaBrowser.Common/Net') 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 +{ + /// + /// Class AlchemyWebSocket + /// + public class AlchemyWebSocket : IWebSocket + { + /// + /// The logger + /// + private static ILogger Logger = LogManager.GetLogger("AlchemyWebSocket"); + + /// + /// Gets or sets the web socket. + /// + /// The web socket. + private UserContext UserContext { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// context + 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); + } + + /// + /// The _disconnected + /// + private bool _disconnected = false; + /// + /// Gets or sets the state. + /// + /// The state. + public WebSocketState State + { + get { return _disconnected ? WebSocketState.Closed : WebSocketState.Open; } + } + + /// + /// Called when [disconnected]. + /// + /// The context. + private void OnDisconnected(UserContext context) + { + _disconnected = true; + } + + /// + /// Called when [receive]. + /// + /// The context. + private void OnReceive(UserContext context) + { + if (OnReceiveDelegate != null) + { + var json = context.DataFrame.ToString(); + + if (!string.IsNullOrWhiteSpace(json)) + { + try + { + var messageResult = JsonSerializer.DeserializeFromString(json); + + OnReceiveDelegate(messageResult); + } + catch (Exception ex) + { + Logger.ErrorException("Error processing web socket message", ex); + } + } + } + } + + /// + /// Sends the async. + /// + /// The bytes. + /// The type. + /// if set to true [end of message]. + /// The cancellation token. + /// Task. + public Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken) + { + return Task.Run(() => UserContext.Send(bytes)); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + } + + /// + /// Gets or sets the receive action. + /// + /// The receive action. + public Action 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 +{ + /// + /// Class BaseRestService + /// + public class BaseRestService : Service, IRestfulService + { + /// + /// The logger + /// + protected ILogger Logger = LogManager.GetLogger("RestService"); + + /// + /// Gets or sets the kernel. + /// + /// The kernel. + public IKernel Kernel { get; set; } + + /// + /// Gets a value indicating whether this instance is range request. + /// + /// true if this instance is range request; otherwise, false. + protected bool IsRangeRequest + { + get + { + return Request.Headers.AllKeys.Contains("Range"); + } + } + + /// + /// Adds the routes. + /// + /// The app host. + public virtual void Configure(IAppHost appHost) + { + } + + /// + /// To the optimized result. + /// + /// + /// The result. + /// System.Object. + /// result + protected object ToOptimizedResult(T result) + where T : class + { + if (result == null) + { + throw new ArgumentNullException("result"); + } + + Response.AddHeader("Vary", "Accept-Encoding"); + + return RequestContext.ToOptimizedResult(result); + } + + /// + /// To the optimized result using cache. + /// + /// + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// The factory fn. + /// System.Object. + /// cacheKey + protected object ToOptimizedResultUsingCache(Guid cacheKey, DateTime lastDateModified, TimeSpan? cacheDuration, Func 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()); + } + + /// + /// To the cached result. + /// + /// + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// The factory fn. + /// Type of the content. + /// System.Object. + /// cacheKey + protected object ToCachedResult(Guid cacheKey, DateTime lastDateModified, TimeSpan? cacheDuration, Func 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(); + } + + /// + /// To the static file result. + /// + /// The path. + /// System.Object. + /// path + 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))); + } + + /// + /// Gets the file stream. + /// + /// The path. + /// Stream. + private Stream GetFileStream(string path) + { + return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous); + } + + /// + /// To the static result. + /// + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// Type of the content. + /// The factory fn. + /// System.Object. + /// cacheKey + protected object ToStaticResult(Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType, Func> 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; + } + + /// + /// Shoulds the compress response. + /// + /// Type of the content. + /// true if XXXX, false otherwise + 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; + } + + /// + /// To the static result. + /// + /// Type of the content. + /// The factory fn. + /// if set to true [compress]. + /// System.Object. + private async Task ToStaticResult(string contentType, Func> 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); + } + + /// + /// Pres the process optimized result. + /// + /// The cache key. + /// The cache key string. + /// The last date modified. + /// Duration of the cache. + /// Type of the content. + /// System.Object. + 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; + } + + /// + /// Determines whether [is not modified] [the specified cache key]. + /// + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// true if [is not modified] [the specified cache key]; otherwise, false. + 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; + } + + /// + /// Determines whether [is not modified] [the specified if modified since]. + /// + /// If modified since. + /// Duration of the cache. + /// The date modified. + /// true if [is not modified] [the specified if modified since]; otherwise, false. + 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; + } + + + /// + /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that + /// + /// The date. + /// DateTime. + private DateTime NormalizeDateForComparison(DateTime date) + { + return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); + } + + /// + /// Sets the caching headers. + /// + /// The cache key. + /// The last date modified. + /// Duration of the cache. + 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); + } + + /// + /// Adds the expires header. + /// + /// The cache key. + /// Duration of the cache. + 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"); + } + } + + /// + /// Adds the age header. + /// + /// The last date modified. + 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 +{ + /// + /// Class BaseActionHandler + /// + /// The type of the T kernel type. + public abstract class BaseActionHandler : BaseSerializationHandler + where TKernelType : IKernel + { + /// + /// Gets the object to serialize. + /// + /// Task{EmptyRequestResult}. + protected override async Task GetObjectToSerialize() + { + await ExecuteAction(); + + return new EmptyRequestResult(); + } + + /// + /// Performs the action. + /// + /// Task. + 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; - } - } - - /// - /// The original HttpListenerContext - /// - protected HttpListenerContext HttpListenerContext { get; set; } - - /// - /// The original QueryString - /// - protected NameValueCollection QueryString - { - get - { - return HttpListenerContext.Request.QueryString; - } - } - - private List> _requestedRanges; - protected IEnumerable> RequestedRanges - { - get - { - if (_requestedRanges == null) - { - _requestedRanges = new List>(); - - 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(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; - } - - /// - /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that - /// - 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 GetResponseInfo(); - - private Hashtable _formValues; - - /// - /// Gets a value from form POST data - /// - protected async Task GetFormValue(string name) - { - if (_formValues == null) - { - _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false); - } - - if (_formValues.ContainsKey(name)) - { - return _formValues[name].ToString(); - } - - return null; - } - - /// - /// Extracts form POST data from a request - /// - private async Task 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 +{ + /// + /// Class BaseHandler + /// + public abstract class BaseHandler : IHttpServerHandler + where TKernelType : IKernel + { + /// + /// Gets the logger. + /// + /// The logger. + protected ILogger Logger { get; private set; } + + /// + /// Initializes the specified kernel. + /// + /// The kernel. + public void Initialize(IKernel kernel) + { + Kernel = (TKernelType)kernel; + Logger = SharedLogger.Logger; + } + + /// + /// Gets or sets the kernel. + /// + /// The kernel. + protected TKernelType Kernel { get; private set; } + + /// + /// Gets the URL suffix used to determine if this handler can process a request. + /// + /// The URL suffix. + 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; + } + } + + /// + /// Handleses the request. + /// + /// The request. + /// true if XXXX, false otherwise + public virtual bool HandlesRequest(HttpListenerRequest request) + { + var name = '/' + UrlSuffix.TrimStart('/'); + + var url = Kernel.WebApplicationName + name; + + return request.Url.LocalPath.EndsWith(url, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Gets or sets the compressed stream. + /// + /// The compressed stream. + private Stream CompressedStream { get; set; } + + /// + /// Gets a value indicating whether [use chunked encoding]. + /// + /// null if [use chunked encoding] contains no value, true if [use chunked encoding]; otherwise, false. + public virtual bool? UseChunkedEncoding + { + get + { + return null; + } + } + + /// + /// The original HttpListenerContext + /// + /// The HTTP listener context. + protected HttpListenerContext HttpListenerContext { get; set; } + + /// + /// The _query string + /// + private NameValueCollection _queryString; + /// + /// The original QueryString + /// + /// The query string. + public NameValueCollection QueryString + { + get + { + // HttpListenerContext.Request.QueryString is not decoded properly + return _queryString ?? (_queryString = HttpUtility.ParseQueryString(HttpListenerContext.Request.Url.Query)); + } + } + + /// + /// The _requested ranges + /// + private List> _requestedRanges; + /// + /// Gets the requested ranges. + /// + /// The requested ranges. + protected IEnumerable> RequestedRanges + { + get + { + if (_requestedRanges == null) + { + _requestedRanges = new List>(); + + 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(start, end)); + } + } + } + + return _requestedRanges; + } + } + + /// + /// Gets a value indicating whether this instance is range request. + /// + /// true if this instance is range request; otherwise, false. + protected bool IsRangeRequest + { + get + { + return HttpListenerContext.Request.Headers.AllKeys.Contains("Range"); + } + } + + /// + /// Gets a value indicating whether [client supports compression]. + /// + /// true if [client supports compression]; otherwise, false. + 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; + } + } + + /// + /// Gets the compression method. + /// + /// The compression method. + 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; + } + } + + /// + /// Processes the request. + /// + /// The CTX. + /// Task. + 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(); + } + } + + /// + /// Appends the error message. + /// + /// The response. + /// The ex. + /// The status code. + 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); + } + } + + /// + /// Processes the request internal. + /// + /// The CTX. + /// Task. + 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; + } + } + + /// + /// The _null task result + /// + private readonly Task _nullTaskResult = Task.FromResult(true); + + /// + /// Called when [processing request]. + /// + /// The response info. + /// Task. + protected virtual Task OnProcessingRequest(ResponseInfo responseInfo) + { + return _nullTaskResult; + } + + /// + /// Logs the response. + /// + /// The CTX. + /// The response info. + 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); + } + } + + /// + /// Processes the uncached request. + /// + /// The CTX. + /// The response info. + /// Task. + 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; + } + } + + /// + /// Configures the caching. + /// + /// The response. + /// The response info. + 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); + } + + /// + /// Adds the expires header. + /// + /// The response. + /// The response info. + 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"; + } + } + + /// + /// Adds the age header. + /// + /// The response. + /// The response info. + 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); + } + } + + /// + /// Writes the response to output stream. + /// + /// The stream. + /// The response info. + /// Length of the content. + /// Task. + protected abstract Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? contentLength); + + /// + /// Disposes the response stream. + /// + 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); + } + } + + /// + /// Determines whether [is cache valid] [the specified if modified since]. + /// + /// If modified since. + /// Duration of the cache. + /// The date modified. + /// true if [is cache valid] [the specified if modified since]; otherwise, false. + 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; + } + + /// + /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that + /// + /// The date. + /// DateTime. + private DateTime NormalizeDateForComparison(DateTime date) + { + return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); + } + + /// + /// Gets the total length of the content. + /// + /// The response info. + /// System.Nullable{System.Int64}. + protected virtual long? GetTotalContentLength(ResponseInfo responseInfo) + { + return null; + } + + /// + /// Gets the response info. + /// + /// Task{ResponseInfo}. + protected abstract Task GetResponseInfo(); + + /// + /// Gets a bool query string param. + /// + /// The name. + /// true if XXXX, false otherwise + protected bool GetBoolQueryStringParam(string name) + { + var val = QueryString[name] ?? string.Empty; + + return val.Equals("1", StringComparison.OrdinalIgnoreCase) || val.Equals("true", StringComparison.OrdinalIgnoreCase); + } + + /// + /// The _form values + /// + private Hashtable _formValues; + + /// + /// Gets a value from form POST data + /// + /// The name. + /// Task{System.String}. + protected async Task GetFormValue(string name) + { + if (_formValues == null) + { + _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false); + } + + if (_formValues.ContainsKey(name)) + { + return _formValues[name].ToString(); + } + + return null; + } + + /// + /// Extracts form POST data from a request + /// + /// The request. + /// Task{Hashtable}. + private async Task 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 + { + /// + /// The logger + /// + internal static ILogger Logger = LogManager.GetLogger("Http Handler"); + } + + /// + /// Class ResponseInfo + /// + public class ResponseInfo + { + /// + /// Gets or sets the type of the content. + /// + /// The type of the content. + public string ContentType { get; set; } + /// + /// Gets or sets the etag. + /// + /// The etag. + public Guid? Etag { get; set; } + /// + /// Gets or sets the date last modified. + /// + /// The date last modified. + public DateTime? DateLastModified { get; set; } + /// + /// Gets or sets the duration of the cache. + /// + /// The duration of the cache. + public TimeSpan CacheDuration { get; set; } + /// + /// Gets or sets a value indicating whether [compress response]. + /// + /// true if [compress response]; otherwise, false. + public bool CompressResponse { get; set; } + /// + /// Gets or sets the status code. + /// + /// The status code. + public int StatusCode { get; set; } + /// + /// Gets or sets a value indicating whether [supports byte range requests]. + /// + /// true if [supports byte range requests]; otherwise, false. + public bool SupportsByteRangeRequests { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public ResponseInfo() + { + CacheDuration = TimeSpan.FromTicks(0); + + CompressResponse = true; + + StatusCode = 200; + } + + /// + /// Gets a value indicating whether this instance is response valid. + /// + /// true if this instance is response valid; otherwise, false. + 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 : 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 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 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 +{ + /// + /// Class BaseSerializationHandler + /// + /// The type of the T kernel type. + /// + public abstract class BaseSerializationHandler : BaseHandler + where TKernelType : IKernel + where T : class + { + /// + /// Gets the serialization format. + /// + /// The serialization format. + public SerializationFormat SerializationFormat + { + get + { + var format = QueryString["dataformat"]; + + if (string.IsNullOrEmpty(format)) + { + return SerializationFormat.Json; + } + + return (SerializationFormat)Enum.Parse(typeof(SerializationFormat), format, true); + } + } + + /// + /// Gets the type of the content. + /// + /// The type of the content. + protected string ContentType + { + get + { + switch (SerializationFormat) + { + case SerializationFormat.Protobuf: + return "application/x-protobuf"; + default: + return MimeTypes.JsonMimeType; + } + } + } + + /// + /// Gets the response info. + /// + /// Task{ResponseInfo}. + protected override Task GetResponseInfo() + { + return Task.FromResult(new ResponseInfo + { + ContentType = ContentType + }); + } + + /// + /// Called when [processing request]. + /// + /// The response info. + /// Task. + protected override async Task OnProcessingRequest(ResponseInfo responseInfo) + { + _objectToSerialize = await GetObjectToSerialize().ConfigureAwait(false); + + if (_objectToSerialize == null) + { + throw new ResourceNotFoundException(); + } + + await base.OnProcessingRequest(responseInfo).ConfigureAwait(false); + } + + /// + /// The _object to serialize + /// + private T _objectToSerialize; + + /// + /// Gets the object to serialize. + /// + /// Task{`0}. + protected abstract Task GetObjectToSerialize(); + + /// + /// Writes the response to output stream. + /// + /// The stream. + /// The response info. + /// Length of the content. + /// Task. + 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; + } + }); + } + } + + /// + /// Enum SerializationFormat + /// + public enum SerializationFormat + { + /// + /// The json + /// + Json, + /// + /// The protobuf + /// + 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 +{ + /// + /// Interface IHttpServerHandler + /// + public interface IHttpServerHandler + { + /// + /// Initializes the specified kernel. + /// + /// The kernel. + void Initialize(IKernel kernel); + + /// + /// Handleses the request. + /// + /// The request. + /// true if XXXX, false otherwise + bool HandlesRequest(HttpListenerRequest request); + + /// + /// Processes the request. + /// + /// The CTX. + /// Task. + 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 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(info); - } - - protected override Task WriteResponseToOutputStream(Stream stream) - { - if (IsRangeRequest) - { - KeyValuePair 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(); - } - } - - /// - /// Handles a range request of "bytes=0-" - /// This will serve the complete content and add the content-range header - /// - private Task ServeCompleteRangeRequest(KeyValuePair 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); - } - - /// - /// Serves a partial range request where the total content length is not known - /// - private async Task ServePartialRangeRequestWithUnknownTotalContentLength(KeyValuePair 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); - } - - /// - /// Serves a partial range request where the total content length is already known - /// - private async Task ServePartialRangeRequestWithKnownTotalContentLength(KeyValuePair 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); - } - - /// - /// Reads bytes from a stream - /// - /// The input stream - /// The starting position - /// The number of bytes to read, or null to read to the end. - private async Task 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 +{ + /// + /// Represents an http handler that serves static content + /// + public class StaticFileHandler : BaseHandler + { + /// + /// Initializes a new instance of the class. + /// + /// The kernel. + public StaticFileHandler(IKernel kernel) + { + Initialize(kernel); + } + + /// + /// The _path + /// + private string _path; + /// + /// Gets or sets the path to the static resource + /// + /// The path. + public string Path + { + get + { + if (!string.IsNullOrWhiteSpace(_path)) + { + return _path; + } + + return QueryString["path"]; + } + set + { + _path = value; + } + } + + /// + /// Gets or sets the last date modified of the resource + /// + /// The last date modified. + public DateTime? LastDateModified { get; set; } + + /// + /// Gets or sets the content type of the resource + /// + /// The type of the content. + public string ContentType { get; set; } + + /// + /// Gets or sets the content type of the resource + /// + /// The etag. + public Guid Etag { get; set; } + + /// + /// Gets or sets the source stream of the resource + /// + /// The source stream. + public Stream SourceStream { get; set; } + + /// + /// Shoulds the compress response. + /// + /// Type of the content. + /// true if XXXX, false otherwise + 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; + } + + /// + /// Gets or sets the duration of the cache. + /// + /// The duration of the cache. + public TimeSpan? CacheDuration { get; set; } + + /// + /// Gets the total length of the content. + /// + /// The response info. + /// System.Nullable{System.Int64}. + 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; + } + + /// + /// Gets the response info. + /// + /// Task{ResponseInfo}. + protected override Task 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); + } + + /// + /// Writes the response to output stream. + /// + /// The stream. + /// The response info. + /// Total length of the content. + /// Task. + 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); + } + + /// + /// Disposes the response stream. + /// + protected override void DisposeResponseStream() + { + if (SourceStream != null) + { + SourceStream.Dispose(); + } + + base.DisposeResponseStream(); + } + + /// + /// Handles a range request of "bytes=0-" + /// This will serve the complete content and add the content-range header + /// + /// The requested range. + /// The response stream. + /// Total length of the content. + /// Task. + private Task ServeCompleteRangeRequest(KeyValuePair 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); + } + + /// + /// Serves a partial range request + /// + /// The range start. + /// The range end. + /// The response stream. + /// Total length of the content. + /// Task. + 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 +{ + /// + /// Class HttpManager + /// + public class HttpManager : BaseManager + { + /// + /// Initializes a new instance of the class. + /// + /// The kernel. + public HttpManager(IKernel kernel) + : base(kernel) + { + } + + /// + /// 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. + /// + /// The HTTP clients. + private readonly ConcurrentDictionary _httpClients = new ConcurrentDictionary(); + + /// + /// Gets + /// + /// The host. + /// HttpClient. + /// host + 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; + } + + /// + /// Performs a GET request and returns the resulting stream + /// + /// The URL. + /// The resource pool. + /// The cancellation token. + /// Task{Stream}. + /// + public async Task 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(); + } + } + + /// + /// Performs a POST request + /// + /// The URL. + /// Params to add to the POST data. + /// The resource pool. + /// The cancellation token. + /// stream on success, null on failure + /// postData + /// + public async Task Post(string url, Dictionary 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(); + } + } + + /// + /// Downloads the contents of a given url into a temporary location + /// + /// The URL. + /// The resource pool. + /// The cancellation token. + /// The progress. + /// The user agent. + /// Task{System.String}. + /// progress + /// + public async Task FetchToTempFile(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken, IProgress 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 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(); + } + } + + /// + /// Downloads the contents of a given url into a MemoryStream + /// + /// The URL. + /// The resource pool. + /// The cancellation token. + /// Task{MemoryStream}. + /// + public async Task 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(); + } + } + + /// + /// Validates the params. + /// + /// The URL. + /// The resource pool. + /// The cancellation token. + /// url + 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"); + } + } + + /// + /// Gets the host from URL. + /// + /// The URL. + /// System.String. + private string GetHostFromUrl(string url) + { + var start = url.IndexOf("://", StringComparison.OrdinalIgnoreCase) + 3; + var len = url.IndexOf('/', start) - start; + return url.Substring(start, len); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool dispose) + { + if (dispose) + { + foreach (var client in _httpClients.Values.ToList()) + { + client.Dispose(); + } + + _httpClients.Clear(); + } + + base.Dispose(dispose); + } + + /// + /// Throws the cancellation exception. + /// + /// The URL. + /// The cancellation token. + /// The exception. + /// Exception. + 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; + } + + /// + /// Ensures the success status code. + /// + /// The response. + /// + 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, IDisposable - { - private readonly HttpListener _listener; - private readonly IObservable _stream; - - public HttpServer(string url) - { - _listener = new HttpListener(); - _listener.Prefixes.Add(url); - _listener.Start(); - _stream = ObservableHttpContext(); - } - - private IObservable ObservableHttpContext() - { - return Observable.Create(obs => - Observable.FromAsync(() => _listener.GetContextAsync()) - .Subscribe(obs)) - .Repeat() - .Retry() - .Publish() - .RefCount(); - } - public void Dispose() - { - _listener.Stop(); - } - - public IDisposable Subscribe(IObserver 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 +{ + /// + /// Class HttpServer + /// + public class HttpServer : HttpListenerBase + { + /// + /// The logger + /// + private static ILogger Logger = Logging.LogManager.GetLogger("HttpServer"); + + /// + /// Gets the URL prefix. + /// + /// The URL prefix. + public string UrlPrefix { get; private set; } + + /// + /// Gets or sets the kernel. + /// + /// The kernel. + private IKernel Kernel { get; set; } + + /// + /// This subscribes to HttpListener requests and finds the appropriate BaseHandler to process it + /// + /// The HTTP listener. + private IDisposable HttpListener { get; set; } + + /// + /// Occurs when [web socket connected]. + /// + public event EventHandler WebSocketConnected; + + /// + /// Gets the default redirect path. + /// + /// The default redirect path. + public string DefaultRedirectPath { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The URL. + /// Name of the product. + /// The kernel. + /// The default redirectpath. + /// urlPrefix + 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); + } + + /// + /// Shut down the Web Service + /// + public override void Stop() + { + if (HttpListener != null) + { + HttpListener.Dispose(); + HttpListener = null; + } + + if (Listener != null) + { + Listener.Prefixes.Remove(UrlPrefix); + } + + base.Stop(); + } + + /// + /// Configures the specified container. + /// + /// The container. + 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(); + } + + /// + /// Starts the Web Service + /// + /// 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. + 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); + } + + /// + /// Creates the observable stream. + /// + /// IObservable{HttpListenerContext}. + private IObservable CreateObservableStream() + { + return Observable.Create(obs => + Observable.FromAsync(() => Listener.GetContextAsync()) + .Subscribe(obs)) + .Repeat() + .Retry() + .Publish() + .RefCount(); + } + + /// + /// Processes incoming http requests by routing them to the appropiate handler + /// + /// The CTX. + 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; + } + + }); + } + + /// + /// Processes the web socket request. + /// + /// The CTX. + /// Task. + 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(); + } + } + + /// + /// Logs the HTTP request. + /// + /// The CTX. + 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); + } + } + + /// + /// Appends the error message. + /// + /// The response. + /// The ex. + /// The status code. + 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); + } + } + + + /// + /// Overridable method that can be used to implement a custom hnandler + /// + /// The context. + /// Cannot execute handler: + handler + at PathInfo: + httpReq.PathInfo + 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); + } + + /// + /// Logs the response. + /// + /// The CTX. + 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); + } + } + + /// + /// Creates the service manager. + /// + /// The assemblies with services. + /// ServiceManager. + protected override ServiceManager CreateServiceManager(params Assembly[] assembliesWithServices) + { + var types = Kernel.RestServices.Select(r => r.GetType()).ToArray(); + + return new ServiceManager(new Container(), new ServiceController(() => types)); + } + } + + /// + /// Class WebSocketConnectEventArgs + /// + public class WebSocketConnectEventArgs : EventArgs + { + /// + /// Gets or sets the web socket. + /// + /// The web socket. + public IWebSocket WebSocket { get; set; } + /// + /// Gets or sets the endpoint. + /// + /// The endpoint. + 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 +{ + /// + /// Interface IRestfulService + /// + 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 +{ + /// + /// Interface IWebSocket + /// + public interface IWebSocket : IDisposable + { + /// + /// Gets or sets the state. + /// + /// The state. + WebSocketState State { get; } + + /// + /// Gets or sets the receive action. + /// + /// The receive action. + Action OnReceiveDelegate { get; set; } + + /// + /// Sends the async. + /// + /// The bytes. + /// The type. + /// if set to true [end of message]. + /// The cancellation token. + /// Task. + 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 +{ + /// + /// Class MimeTypes + /// + public static class MimeTypes + { + /// + /// The json MIME type + /// + public static string JsonMimeType = "application/json"; + + /// + /// Gets the type of the MIME. + /// + /// The path. + /// System.String. + /// path + /// Argument not supported: + path + 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 +{ + /// + /// Class NativeWebSocket + /// + public class NativeWebSocket : IWebSocket + { + /// + /// The logger + /// + private static ILogger Logger = LogManager.GetLogger("NativeWebSocket"); + + /// + /// Gets or sets the web socket. + /// + /// The web socket. + private WebSocket WebSocket { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The socket. + /// socket + public NativeWebSocket(WebSocket socket) + { + if (socket == null) + { + throw new ArgumentNullException("socket"); + } + + WebSocket = socket; + + Receive(); + } + + /// + /// Gets or sets the state. + /// + /// The state. + public WebSocketState State + { + get { return WebSocket.State; } + } + + /// + /// Receives this instance. + /// + 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(memoryStream); + + OnReceiveDelegate(messageResult); + } + catch (Exception ex) + { + Logger.ErrorException("Error processing web socket message", ex); + } + } + } + } + } + + /// + /// Receives the async. + /// + /// The cancellation token. + /// Task{WebSocketMessageInfo}. + /// Connection closed + private async Task ReceiveBytesAsync(CancellationToken cancellationToken) + { + var bytes = new byte[4096]; + var buffer = new ArraySegment(bytes); + + var result = await WebSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); + + if (result.CloseStatus.HasValue) + { + throw new WebSocketException("Connection closed"); + } + + return buffer.Array; + } + + /// + /// Sends the async. + /// + /// The bytes. + /// The type. + /// if set to true [end of message]. + /// The cancellation token. + /// Task. + public Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken) + { + return WebSocket.SendAsync(new ArraySegment(bytes), type, true, cancellationToken); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + WebSocket.Dispose(); + } + } + + /// + /// Gets or sets the receive action. + /// + /// The receive action. + public Action 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 +{ + /// + /// Class NetUtils + /// + public static class NetUtils + { + /// + /// Gets the machine's local ip address + /// + /// IPAddress. + public static IPAddress GetLocalIpAddress() + { + var host = Dns.GetHostEntry(Dns.GetHostName()); + + return host.AddressList.FirstOrDefault(i => i.AddressFamily == AddressFamily.InterNetwork); + } + + /// + /// Gets a random port number that is currently available + /// + /// System.Int32. + public static int GetRandomUnusedPort() + { + var listener = new TcpListener(IPAddress.Any, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + /// + /// Creates the netsh URL registration. + /// + /// The URL prefix. + 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(); + } + } + + /// + /// Adds the windows firewall rule. + /// + /// The port. + /// The protocol. + 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); + } + + /// + /// Removes the windows firewall rule. + /// + /// The port. + /// The protocol. + 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); + } + + /// + /// Runs the netsh. + /// + /// The args. + 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(); + } + } + + /// + /// Returns MAC Address from first Network Card in Computer + /// + /// [string] MAC Address + 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(":", ""); + } + + /// + /// 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 + /// + /// Arraylist that represents all the SV_TYPE_WORKSTATION and SV_TYPE_SERVER + /// PC's in the Domain + public static IEnumerable 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); + } + } + } + + /// + /// Enum NetworkProtocol + /// + public enum NetworkProtocol + { + /// + /// The TCP + /// + Tcp, + /// + /// The UDP + /// + 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 +{ + /// + /// Type of share + /// + [Flags] + public enum ShareType + { + /// Disk share + Disk = 0, + /// Printer share + Printer = 1, + /// Device share + Device = 2, + /// IPC share + IPC = 3, + /// Special share + Special = -2147483648, // 0x80000000, + } + + #region Share + + /// + /// Information about a local share + /// + public class Share + { + #region Private data + + private string _server; + private string _netName; + private string _path; + private ShareType _shareType; + private string _remark; + + #endregion + + #region Constructor + + /// + /// Constructor + /// + /// + /// + 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 + + /// + /// The name of the computer that this share belongs to + /// + public string Server + { + get { return _server; } + } + + /// + /// Share name + /// + public string NetName + { + get { return _netName; } + } + + /// + /// Local path + /// + public string Path + { + get { return _path; } + } + + /// + /// Share type + /// + public ShareType ShareType + { + get { return _shareType; } + } + + /// + /// Comment + /// + public string Remark + { + get { return _remark; } + } + + /// + /// Returns true if this is a file system share + /// + 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; + } + } + + /// + /// Get the root of a disk-based share + /// + 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 + + /// + /// Returns the path to this share + /// + /// + 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); + } + + /// + /// Returns true if this share matches the local path + /// + /// + /// + public bool MatchesPath(string path) + { + if (!IsFileSystem) return false; + if (null == path || 0 == path.Length) return true; + + return path.ToLower().StartsWith(_path.ToLower()); + } + } + + #endregion + + /// + /// A collection of shares + /// + public class ShareCollection : ReadOnlyCollectionBase + { + #region Platform + + /// + /// Is this an NT platform? + /// + protected static bool IsNT + { + get { return (PlatformID.Win32NT == Environment.OSVersion.Platform); } + } + + /// + /// Returns true if this is Windows 2000 or higher + /// + 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 + + /// Maximum path length + protected const int MAX_PATH = 260; + /// No error + protected const int NO_ERROR = 0; + /// Access denied + protected const int ERROR_ACCESS_DENIED = 5; + /// Access denied + protected const int ERROR_WRONG_LEVEL = 124; + /// More data available + protected const int ERROR_MORE_DATA = 234; + /// Not connected + protected const int ERROR_NOT_CONNECTED = 2250; + /// Level 1 + protected const int UNIVERSAL_NAME_INFO_LEVEL = 1; + /// Max extries (9x) + protected const int MAX_SI50_ENTRIES = 20; + + #endregion + + #region Structures + + /// Unc name + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + protected struct UNIVERSAL_NAME_INFO + { + [MarshalAs(UnmanagedType.LPTStr)] + public string lpUniversalName; + } + + /// Share information, NT, level 2 + /// + /// Requires admin rights to work. + /// + [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; + } + + /// Share information, NT, level 1 + /// + /// Fallback when no admin rights. + /// + [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; + } + + /// Share information, Win9x + [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); } + } + } + + /// Share information level 1, Win9x + [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 + + /// Get a UNC name + [DllImport("mpr", CharSet = CharSet.Auto)] + protected static extern int WNetGetUniversalName(string lpLocalPath, + int dwInfoLevel, ref UNIVERSAL_NAME_INFO lpBuffer, ref int lpBufferSize); + + /// Get a UNC name + [DllImport("mpr", CharSet = CharSet.Auto)] + protected static extern int WNetGetUniversalName(string lpLocalPath, + int dwInfoLevel, IntPtr lpBuffer, ref int lpBufferSize); + + /// Enumerate shares (NT) + [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); + + /// Enumerate shares (9x) + [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); + + /// Free the buffer (NT) + [DllImport("netapi32")] + protected static extern int NetApiBufferFree(IntPtr lpBuffer); + + #endregion + + #region Enumerate shares + + /// + /// Enumerates the shares on Windows NT + /// + /// The server name + /// The ShareCollection + 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); + } + } + + /// + /// Enumerates the shares on Windows 9x + /// + /// The server name + /// The ShareCollection + 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); + } + } + + /// + /// Enumerates the shares + /// + /// The server name + /// The ShareCollection + 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 + + /// + /// Returns true if fileName is a valid local file-name of the form: + /// X:\, where X is a drive letter from A-Z + /// + /// The filename to check + /// + 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 + + /// The name of the server this collection represents + private string _server; + + #region Constructor + + /// + /// Default constructor - local machine + /// + public ShareCollection() + { + _server = string.Empty; + EnumerateShares(_server, this); + } + + /// + /// Constructor + /// + /// + 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 + + /// + /// Returns the name of the server this collection represents + /// + public string Server + { + get { return _server; } + } + + /// + /// Returns the at the specified index. + /// + public Share this[int index] + { + get { return (Share)InnerList[index]; } + } + + /// + /// Returns the which matches a given local path + /// + /// The path to match + 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 + + /// + /// Copy this collection to an array + /// + /// + /// + 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> 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 +{ + /// + /// Provides a Udp Server + /// + public class UdpServer : IObservable, IDisposable + { + /// + /// The _udp client + /// + private readonly UdpClient _udpClient; + /// + /// The _stream + /// + private readonly IObservable _stream; + + /// + /// Initializes a new instance of the class. + /// + /// The end point. + /// endPoint + 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(); + } + + /// + /// Creates the observable. + /// + /// IObservable{UdpReceiveResult}. + private IObservable CreateObservable() + { + return Observable.Create(obs => + Observable.FromAsync(() => _udpClient.ReceiveAsync()) + .Subscribe(obs)) + .Repeat() + .Retry() + .Publish() + .RefCount(); + } + + /// + /// Subscribes the specified observer. + /// + /// The observer. + /// IDisposable. + /// observer + public IDisposable Subscribe(IObserver observer) + { + if (observer == null) + { + throw new ArgumentNullException("observer"); + } + + return _stream.Subscribe(observer); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + _udpClient.Close(); + } + } + + /// + /// Sends the async. + /// + /// The data. + /// The end point. + /// Task{System.Int32}. + /// data + public async Task 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); + } + + /// + /// Sends the async. + /// + /// The bytes. + /// The end point. + /// Task{System.Int32}. + /// bytes + public async Task 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 +{ + /// + /// Class WebSocketConnection + /// + public class WebSocketConnection : IDisposable + { + /// + /// The _socket + /// + private readonly IWebSocket _socket; + + /// + /// The _remote end point + /// + public readonly EndPoint RemoteEndPoint; + + /// + /// The _cancellation token source + /// + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + + /// + /// The _send semaphore + /// + private readonly SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1,1); + + /// + /// The logger + /// + private static readonly ILogger Logger = LogManager.GetLogger("WebSocketConnection"); + + /// + /// Initializes a new instance of the class. + /// + /// The socket. + /// The remote end point. + /// The receive action. + /// socket + public WebSocketConnection(IWebSocket socket, EndPoint remoteEndPoint, Action 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; + } + + /// + /// Called when [receive]. + /// + /// The info. + /// The callback. + private void OnReceive(WebSocketMessageInfo info, Action callback) + { + try + { + info.Connection = this; + + callback(info); + } + catch (Exception ex) + { + Logger.ErrorException("Error processing web socket message", ex); + } + } + + /// + /// Sends a message asynchronously. + /// + /// + /// The message. + /// The cancellation token. + /// Task. + /// message + public Task SendAsync(WebSocketMessage message, CancellationToken cancellationToken) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + var bytes = JsonSerializer.SerializeToBytes(message); + + return SendAsync(bytes, cancellationToken); + } + + /// + /// Sends a message asynchronously. + /// + /// The buffer. + /// The cancellation token. + /// Task. + public Task SendAsync(byte[] buffer, CancellationToken cancellationToken) + { + return SendAsync(buffer, WebSocketMessageType.Text, cancellationToken); + } + + /// + /// Sends a message asynchronously. + /// + /// The buffer. + /// The type. + /// The cancellation token. + /// Task. + /// buffer + 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(); + } + } + + /// + /// Gets the state. + /// + /// The state. + public WebSocketState State + { + get { return _socket.State; } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + _cancellationTokenSource.Dispose(); + _socket.Dispose(); + } + } + } + + /// + /// Class WebSocketMessage + /// + /// + public class WebSocketMessage + { + /// + /// Gets or sets the type of the message. + /// + /// The type of the message. + public string MessageType { get; set; } + /// + /// Gets or sets the data. + /// + /// The data. + public T Data { get; set; } + } + + /// + /// Class WebSocketMessageInfo + /// + public class WebSocketMessageInfo : WebSocketMessage + { + /// + /// Gets or sets the connection. + /// + /// The connection. + public WebSocketConnection Connection { get; set; } + } +} -- cgit v1.2.3