From 51b3c32e2cc84778212531d72665d2c5f567f34a Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Tue, 26 Feb 2013 16:05:52 -0500 Subject: a little more consolidation --- .../HttpClientManager/HttpClientManager.cs | 482 +++++++ .../HttpServer/BaseRestService.cs | 452 +++++++ .../HttpServer/HttpServer.cs | 551 ++++++++ .../HttpServer/NativeWebSocket.cs | 165 +++ .../HttpServer/ServerFactory.cs | 27 + .../MediaBrowser.Common.Implementations.csproj | 79 +- .../NetworkManagement/NativeMethods.cs | 72 ++ .../NetworkManagement/NetworkManager.cs | 377 ++++++ .../NetworkManagement/NetworkShares.cs | 638 ++++++++++ MediaBrowser.Common.Implementations/README.txt | 41 + .../Server/RegisterServer.bat | 28 - .../Server/ServerManager.cs | 520 -------- .../Server/WebSocketConnection.cs | 225 ---- .../ServerManager/RegisterServer.bat | 28 + .../ServerManager/ServerManager.cs | 520 ++++++++ .../ServerManager/WebSocketConnection.cs | 225 ++++ .../Udp/UdpServer.cs | 203 +++ .../WebSocket/AlchemyServer.cs | 113 ++ .../WebSocket/AlchemyWebSocket.cs | 132 ++ .../packages.config | 10 + .../swagger-ui/css/screen.css | 959 ++++++++++++++ .../swagger-ui/images/pet_store_api.png | Bin 0 -> 824 bytes .../swagger-ui/images/wordnik_api.png | Bin 0 -> 980 bytes .../swagger-ui/index.html | 88 ++ .../swagger-ui/lib/backbone-min.js | 38 + .../lib/handlebars.runtime-1.0.0.beta.6.js | 223 ++++ .../swagger-ui/lib/jquery.ba-bbq.min.js | 18 + .../swagger-ui/lib/jquery.min.js | 4 + .../swagger-ui/lib/jquery.slideto.min.js | 1 + .../swagger-ui/lib/jquery.wiggle.min.js | 8 + .../swagger-ui/lib/swagger.js | 507 ++++++++ .../swagger-ui/lib/underscore-min.js | 32 + .../swagger-ui/swagger-ui.js | 1309 ++++++++++++++++++++ .../swagger-ui/swagger-ui.min.js | 1 + 34 files changed, 7300 insertions(+), 776 deletions(-) create mode 100644 MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs create mode 100644 MediaBrowser.Common.Implementations/HttpServer/BaseRestService.cs create mode 100644 MediaBrowser.Common.Implementations/HttpServer/HttpServer.cs create mode 100644 MediaBrowser.Common.Implementations/HttpServer/NativeWebSocket.cs create mode 100644 MediaBrowser.Common.Implementations/HttpServer/ServerFactory.cs create mode 100644 MediaBrowser.Common.Implementations/NetworkManagement/NativeMethods.cs create mode 100644 MediaBrowser.Common.Implementations/NetworkManagement/NetworkManager.cs create mode 100644 MediaBrowser.Common.Implementations/NetworkManagement/NetworkShares.cs create mode 100644 MediaBrowser.Common.Implementations/README.txt delete mode 100644 MediaBrowser.Common.Implementations/Server/RegisterServer.bat delete mode 100644 MediaBrowser.Common.Implementations/Server/ServerManager.cs delete mode 100644 MediaBrowser.Common.Implementations/Server/WebSocketConnection.cs create mode 100644 MediaBrowser.Common.Implementations/ServerManager/RegisterServer.bat create mode 100644 MediaBrowser.Common.Implementations/ServerManager/ServerManager.cs create mode 100644 MediaBrowser.Common.Implementations/ServerManager/WebSocketConnection.cs create mode 100644 MediaBrowser.Common.Implementations/Udp/UdpServer.cs create mode 100644 MediaBrowser.Common.Implementations/WebSocket/AlchemyServer.cs create mode 100644 MediaBrowser.Common.Implementations/WebSocket/AlchemyWebSocket.cs create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/css/screen.css create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/images/pet_store_api.png create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/images/wordnik_api.png create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/index.html create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/lib/backbone-min.js create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/lib/handlebars.runtime-1.0.0.beta.6.js create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/lib/jquery.ba-bbq.min.js create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/lib/jquery.min.js create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/lib/jquery.slideto.min.js create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/lib/jquery.wiggle.min.js create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/lib/swagger.js create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/lib/underscore-min.js create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/swagger-ui.js create mode 100644 MediaBrowser.Common.Implementations/swagger-ui/swagger-ui.min.js (limited to 'MediaBrowser.Common.Implementations') diff --git a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs new file mode 100644 index 0000000000..f542165e4c --- /dev/null +++ b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs @@ -0,0 +1,482 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Kernel; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +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.Implementations.HttpClientManager +{ + /// + /// Class HttpClientManager + /// + public class HttpClientManager : IHttpClient + { + /// + /// The _logger + /// + private readonly ILogger _logger; + + /// + /// The _app paths + /// + private readonly IApplicationPaths _appPaths; + + /// + /// Initializes a new instance of the class. + /// + /// The kernel. + /// The logger. + public HttpClientManager(IApplicationPaths appPaths, ILogger logger) + { + if (appPaths == null) + { + throw new ArgumentNullException("appPaths"); + } + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + _logger = logger; + _appPaths = appPaths; + } + + /// + /// 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("HttpClientManager.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("HttpClientManager.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 GetTempFile(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(_appPaths.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("HttpClientManager.GetTempFile 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 GetMemoryStream(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("HttpClientManager.GetMemoryStream 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); + } + + /// + /// 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) + { + foreach (var client in _httpClients.Values.ToList()) + { + client.Dispose(); + } + + _httpClients.Clear(); + } + } + + /// + /// 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.Implementations/HttpServer/BaseRestService.cs b/MediaBrowser.Common.Implementations/HttpServer/BaseRestService.cs new file mode 100644 index 0000000000..bcf1dc4a7f --- /dev/null +++ b/MediaBrowser.Common.Implementations/HttpServer/BaseRestService.cs @@ -0,0 +1,452 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Kernel; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using ServiceStack.Common; +using ServiceStack.Common.Web; +using ServiceStack.ServiceHost; +using ServiceStack.ServiceInterface; +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using MimeTypes = MediaBrowser.Common.Net.MimeTypes; +using StreamWriter = MediaBrowser.Common.Net.StreamWriter; + +namespace MediaBrowser.Common.Implementations.HttpServer +{ + /// + /// Class BaseRestService + /// + public class BaseRestService : Service, IRestfulService + { + /// + /// Gets or sets the kernel. + /// + /// The kernel. + public IKernel Kernel { get; set; } + + /// + /// Gets or sets the logger. + /// + /// The logger. + public ILogger Logger { 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"); + } + } + + /// + /// 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; + } + if (contentType.StartsWith("application/", 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; + + var stream = await factoryFn().ConfigureAwait(false); + + return new StreamWriter(stream); + } + + string content; + + using (var stream = await factoryFn().ConfigureAwait(false)) + { + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync().ConfigureAwait(false); + } + } + + var contents = content.Compress(RequestContext.CompressionType); + + return new CompressedResult(contents, RequestContext.CompressionType, contentType); + } + + /// + /// 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.Implementations/HttpServer/HttpServer.cs b/MediaBrowser.Common.Implementations/HttpServer/HttpServer.cs new file mode 100644 index 0000000000..57ba6163ba --- /dev/null +++ b/MediaBrowser.Common.Implementations/HttpServer/HttpServer.cs @@ -0,0 +1,551 @@ +using Funq; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Kernel; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using ServiceStack.Api.Swagger; +using ServiceStack.Common.Web; +using ServiceStack.Configuration; +using ServiceStack.Logging.NLogger; +using ServiceStack.ServiceHost; +using ServiceStack.ServiceInterface.Cors; +using ServiceStack.Text; +using ServiceStack.WebHost.Endpoints; +using ServiceStack.WebHost.Endpoints.Extensions; +using ServiceStack.WebHost.Endpoints.Support; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Reactive.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Implementations.HttpServer +{ + /// + /// Class HttpServer + /// + public class HttpServer : HttpListenerBase, IHttpServer + { + /// + /// The logger + /// + private readonly ILogger _logger; + + /// + /// Gets the URL prefix. + /// + /// The URL prefix. + public string UrlPrefix { get; private set; } + + /// + /// The _rest services + /// + private readonly List _restServices = new List(); + + /// + /// Gets or sets the application host. + /// + /// The application host. + private IApplicationHost ApplicationHost { get; set; } + + /// + /// This subscribes to HttpListener requests and finds the appropriate BaseHandler to process it + /// + /// The HTTP listener. + private IDisposable HttpListener { get; set; } + + /// + /// Gets or sets the protobuf serializer. + /// + /// The protobuf serializer. + private IProtobufSerializer ProtobufSerializer { get; set; } + + /// + /// Occurs when [web socket connected]. + /// + public event EventHandler WebSocketConnected; + + /// + /// Gets the default redirect path. + /// + /// The default redirect path. + private string DefaultRedirectPath { get; set; } + + /// + /// Gets or sets the name of the server. + /// + /// The name of the server. + private string ServerName { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The application host. + /// The protobuf serializer. + /// The logger. + /// Name of the server. + /// The default redirectpath. + /// urlPrefix + public HttpServer(IApplicationHost applicationHost, IProtobufSerializer protobufSerializer, ILogger logger, string serverName, string defaultRedirectpath) + : base() + { + if (protobufSerializer == null) + { + throw new ArgumentNullException("protobufSerializer"); + } + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + if (applicationHost == null) + { + throw new ArgumentNullException("applicationHost"); + } + if (string.IsNullOrEmpty(serverName)) + { + throw new ArgumentNullException("serverName"); + } + if (string.IsNullOrEmpty(defaultRedirectpath)) + { + throw new ArgumentNullException("defaultRedirectpath"); + } + + ServerName = serverName; + DefaultRedirectPath = defaultRedirectpath; + ProtobufSerializer = protobufSerializer; + _logger = logger; + ApplicationHost = applicationHost; + + EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath = null; + EndpointHostConfig.Instance.MetadataRedirectPath = "metadata"; + } + + /// + /// Configures the specified container. + /// + /// The container. + public override void Configure(Container container) + { + JsConfig.DateHandler = JsonDateHandler.ISO8601; + JsConfig.ExcludeTypeInfo = true; + JsConfig.IncludeNullValues = false; + + SetConfig(new EndpointHostConfig + { + DefaultRedirectPath = DefaultRedirectPath, + + // Tell SS to bubble exceptions up to here + WriteErrorsToResponse = false, + + DebugMode = true + }); + + container.Adapter = new ContainerAdapter(ApplicationHost); + + Plugins.Add(new SwaggerFeature()); + Plugins.Add(new CorsFeature()); + + ServiceStack.Logging.LogManager.LogFactory = new NLogFactory(); + } + + /// + /// 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) + { + if (string.IsNullOrEmpty(urlBase)) + { + throw new ArgumentNullException("urlBase"); + } + + // *** Already running - just leave it in place + if (IsStarted) + { + return; + } + + if (Listener == null) + { + Listener = new HttpListener(); + } + + EndpointHost.Config.ServiceStackHandlerFactoryPath = HttpListenerRequestWrapper.GetHandlerPathIfAny(urlBase); + + UrlPrefix = urlBase; + + Listener.Prefixes.Add(urlBase); + + IsStarted = true; + Listener.Start(); + + HttpListener = CreateObservableStream().Subscribe(ProcessHttpRequestAsync); + } + + /// + /// 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; + } + + + Task.Run(() => + { + RaiseReceiveWebRequest(context); + + try + { + ProcessRequest(context); + } + catch (InvalidOperationException ex) + { + HandleException(context.Response, ex, 422); + + throw; + } + catch (ResourceNotFoundException ex) + { + HandleException(context.Response, ex, 404); + + throw; + } + catch (FileNotFoundException ex) + { + HandleException(context.Response, ex, 404); + + throw; + } + catch (DirectoryNotFoundException ex) + { + HandleException(context.Response, ex, 404); + + throw; + } + catch (UnauthorizedAccessException ex) + { + HandleException(context.Response, ex, 401); + + throw; + } + catch (ArgumentException ex) + { + HandleException(context.Response, ex, 400); + + throw; + } + catch (Exception ex) + { + HandleException(context.Response, ex, 500); + + throw; + } + }); + } + + /// + /// 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, _logger), Endpoint = ctx.Request.RemoteEndPoint.ToString() }); + } + } + 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 (EnableHttpRequestLogging) + { + _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 (EnableHttpRequestLogging) + { + _logger.LogMultiline(msg, LogSeverity.Debug, log); + } + } + + /// + /// Creates the service manager. + /// + /// The assemblies with services. + /// ServiceManager. + protected override ServiceManager CreateServiceManager(params Assembly[] assembliesWithServices) + { + var types = _restServices.Select(r => r.GetType()).ToArray(); + + return new ServiceManager(new Container(), new ServiceController(() => types)); + } + + /// + /// 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(); + } + + /// + /// The _supports native web socket + /// + private bool? _supportsNativeWebSocket; + + /// + /// Gets a value indicating whether [supports web sockets]. + /// + /// true if [supports web sockets]; otherwise, false. + public bool SupportsWebSockets + { + get + { + if (!_supportsNativeWebSocket.HasValue) + { + try + { + new ClientWebSocket(); + + _supportsNativeWebSocket = true; + } + catch (PlatformNotSupportedException) + { + _supportsNativeWebSocket = false; + } + } + + return _supportsNativeWebSocket.Value; + } + } + + + /// + /// Gets or sets a value indicating whether [enable HTTP request logging]. + /// + /// true if [enable HTTP request logging]; otherwise, false. + public bool EnableHttpRequestLogging { get; set; } + + /// + /// Adds the rest handlers. + /// + /// The services. + public void Init(IEnumerable services) + { + _restServices.AddRange(services); + + EndpointHost.ConfigureHost(this, ServerName, CreateServiceManager()); + ContentTypeFilters.Register(ContentType.ProtoBuf, (reqCtx, res, stream) => ProtobufSerializer.SerializeToStream(res, stream), (type, stream) => ProtobufSerializer.DeserializeFromStream(stream, type)); + + Init(); + } + } + + /// + /// Class ContainerAdapter + /// + class ContainerAdapter : IContainerAdapter + { + /// + /// The _app host + /// + private readonly IApplicationHost _appHost; + + /// + /// Initializes a new instance of the class. + /// + /// The app host. + public ContainerAdapter(IApplicationHost appHost) + { + _appHost = appHost; + } + /// + /// Resolves this instance. + /// + /// + /// ``0. + public T Resolve() + { + return _appHost.Resolve(); + } + + /// + /// Tries the resolve. + /// + /// + /// ``0. + public T TryResolve() + { + return _appHost.TryResolve(); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Common.Implementations/HttpServer/NativeWebSocket.cs b/MediaBrowser.Common.Implementations/HttpServer/NativeWebSocket.cs new file mode 100644 index 0000000000..97bab96f85 --- /dev/null +++ b/MediaBrowser.Common.Implementations/HttpServer/NativeWebSocket.cs @@ -0,0 +1,165 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using WebSocketMessageType = MediaBrowser.Common.Net.WebSocketMessageType; +using WebSocketState = MediaBrowser.Common.Net.WebSocketState; + +namespace MediaBrowser.Common.Implementations.HttpServer +{ + /// + /// Class NativeWebSocket + /// + public class NativeWebSocket : IWebSocket + { + /// + /// The logger + /// + private readonly ILogger _logger; + + /// + /// Gets or sets the web socket. + /// + /// The web socket. + private System.Net.WebSockets.WebSocket WebSocket { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The socket. + /// The logger. + /// socket + public NativeWebSocket(System.Net.WebSockets.WebSocket socket, ILogger logger) + { + if (socket == null) + { + throw new ArgumentNullException("socket"); + } + + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + _logger = logger; + WebSocket = socket; + + Receive(); + } + + /// + /// Gets or sets the state. + /// + /// The state. + public WebSocketState State + { + get + { + WebSocketState commonState; + + if (!Enum.TryParse(WebSocket.State.ToString(), true, out commonState)) + { + _logger.Warn("Unrecognized WebSocketState: {0}", WebSocket.State.ToString()); + } + + return commonState; + } + } + + /// + /// 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) + { + OnReceiveDelegate(bytes); + } + } + } + + /// + /// 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) + { + System.Net.WebSockets.WebSocketMessageType nativeType; + + if (!Enum.TryParse(type.ToString(), true, out nativeType)) + { + _logger.Warn("Unrecognized WebSocketMessageType: {0}", type.ToString()); + } + + return WebSocket.SendAsync(new ArraySegment(bytes), nativeType, 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.Implementations/HttpServer/ServerFactory.cs b/MediaBrowser.Common.Implementations/HttpServer/ServerFactory.cs new file mode 100644 index 0000000000..743bd60c4c --- /dev/null +++ b/MediaBrowser.Common.Implementations/HttpServer/ServerFactory.cs @@ -0,0 +1,27 @@ +using MediaBrowser.Common.Kernel; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Common.Implementations.HttpServer +{ + /// + /// Class ServerFactory + /// + public static class ServerFactory + { + /// + /// Creates the server. + /// + /// The application host. + /// The protobuf serializer. + /// The logger. + /// Name of the server. + /// The default redirectpath. + /// IHttpServer. + public static IHttpServer CreateServer(IApplicationHost applicationHost, IProtobufSerializer protobufSerializer, ILogger logger, string serverName, string defaultRedirectpath) + { + return new HttpServer(applicationHost, protobufSerializer, logger, serverName, defaultRedirectpath); + } + } +} diff --git a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj index 52be41ddde..44b4e7df76 100644 --- a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj +++ b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj @@ -35,12 +35,43 @@ Always + + ..\packages\Alchemy.2.2.1\lib\net40\Alchemy.dll + ..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll ..\packages\protobuf-net.2.0.0.621\lib\net40\protobuf-net.dll + + ..\packages\ServiceStack.3.9.37\lib\net35\ServiceStack.dll + + + ..\packages\ServiceStack.Api.Swagger.3.9.35\lib\net35\ServiceStack.Api.Swagger.dll + + + ..\packages\ServiceStack.Common.3.9.37\lib\net35\ServiceStack.Common.dll + + + False + ..\packages\ServiceStack.Common.3.9.37\lib\net35\ServiceStack.Interfaces.dll + + + ..\packages\ServiceStack.Logging.NLog.1.0.6.0\lib\net35\ServiceStack.Logging.NLog.dll + + + ..\packages\ServiceStack.OrmLite.SqlServer.3.9.37\lib\ServiceStack.OrmLite.dll + + + ..\packages\ServiceStack.OrmLite.SqlServer.3.9.37\lib\ServiceStack.OrmLite.SqlServer.dll + + + ..\packages\ServiceStack.Redis.3.9.37\lib\net35\ServiceStack.Redis.dll + + + ..\packages\ServiceStack.3.9.37\lib\net35\ServiceStack.ServiceInterface.dll + ..\packages\ServiceStack.Text.3.9.37\lib\net35\ServiceStack.Text.dll @@ -51,6 +82,20 @@ + + + + + + ..\packages\Rx-Core.2.1.30214.0\lib\Net45\System.Reactive.Core.dll + + + ..\packages\Rx-Interfaces.2.1.30214.0\lib\Net45\System.Reactive.Interfaces.dll + + + ..\packages\Rx-Linq.2.1.30214.0\lib\Net45\System.Reactive.Linq.dll + + @@ -59,9 +104,17 @@ + + + + + + + + @@ -72,8 +125,11 @@ - - + + + + + @@ -87,7 +143,24 @@ - + + + + + + + + + + + + + + + + + + diff --git a/MediaBrowser.Common.Implementations/NetworkManagement/NativeMethods.cs b/MediaBrowser.Common.Implementations/NetworkManagement/NativeMethods.cs new file mode 100644 index 0000000000..7c8a094937 --- /dev/null +++ b/MediaBrowser.Common.Implementations/NetworkManagement/NativeMethods.cs @@ -0,0 +1,72 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace MediaBrowser.Common.Implementations.NetworkManagement +{ + /// + /// Class NativeMethods + /// + [SuppressUnmanagedCodeSecurity] + public static class NativeMethods + { + //declare the Netapi32 : NetServerEnum method import + /// + /// Nets the server enum. + /// + /// Name of the server. + /// The dw level. + /// The p buf. + /// The dw pref max len. + /// The dw entries read. + /// The dw total entries. + /// Type of the dw server. + /// The domain. + /// The dw resume handle. + /// System.Int32. + [DllImport("Netapi32", CharSet = CharSet.Auto, SetLastError = true), + SuppressUnmanagedCodeSecurity] + + public static extern int NetServerEnum( + string ServerName, // must be null + int dwLevel, + ref IntPtr pBuf, + int dwPrefMaxLen, + out int dwEntriesRead, + out int dwTotalEntries, + int dwServerType, + string domain, // null for login domain + out int dwResumeHandle + ); + + //declare the Netapi32 : NetApiBufferFree method import + /// + /// Nets the API buffer free. + /// + /// The p buf. + /// System.Int32. + [DllImport("Netapi32", SetLastError = true), + SuppressUnmanagedCodeSecurity] + + public static extern int NetApiBufferFree( + IntPtr pBuf); + } + + //create a _SERVER_INFO_100 STRUCTURE + /// + /// Struct _SERVER_INFO_100 + /// + [StructLayout(LayoutKind.Sequential)] + public struct _SERVER_INFO_100 + { + /// + /// The sv100_platform_id + /// + internal int sv100_platform_id; + /// + /// The sv100_name + /// + [MarshalAs(UnmanagedType.LPWStr)] + internal string sv100_name; + } +} diff --git a/MediaBrowser.Common.Implementations/NetworkManagement/NetworkManager.cs b/MediaBrowser.Common.Implementations/NetworkManagement/NetworkManager.cs new file mode 100644 index 0000000000..51b37944c3 --- /dev/null +++ b/MediaBrowser.Common.Implementations/NetworkManagement/NetworkManager.cs @@ -0,0 +1,377 @@ +using System.Management; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Net; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace MediaBrowser.Common.Implementations.NetworkManagement +{ + /// + /// Class NetUtils + /// + public class NetworkManager : INetworkManager + { + /// + /// Gets the machine's local ip address + /// + /// IPAddress. + public string GetLocalIpAddress() + { + var host = Dns.GetHostEntry(Dns.GetHostName()); + + var ip = host.AddressList.FirstOrDefault(i => i.AddressFamily == AddressFamily.InterNetwork); + + if (ip == null) + { + return null; + } + + return ip.ToString(); + } + + /// + /// Gets a random port number that is currently available + /// + /// System.Int32. + public 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. + /// + public void AuthorizeHttpListening(string url) + { + var startInfo = new ProcessStartInfo + { + FileName = "netsh", + Arguments = string.Format("http add urlacl url={0} user=\"NT AUTHORITY\\Authenticated Users\"", url), + 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 void AddSystemFirewallRule(int port, NetworkProtocol protocol) + { + // First try to remove it so we don't end up creating duplicates + RemoveSystemFirewallRule(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 void RemoveSystemFirewallRule(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 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 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 IEnumerable GetNetworkDevices() + { + //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); + } + } + + + /// + /// Gets the network shares. + /// + /// The path. + /// IEnumerable{NetworkShare}. + public IEnumerable GetNetworkShares(string path) + { + return new ShareCollection(path).OfType().Select(ToNetworkShare); + } + + /// + /// To the network share. + /// + /// The share. + /// NetworkShare. + private NetworkShare ToNetworkShare(Share share) + { + return new NetworkShare + { + Name = share.NetName, + Path = share.Path, + Remark = share.Remark, + Server = share.Server, + ShareType = ToNetworkShareType(share.ShareType) + }; + } + + /// + /// To the type of the network share. + /// + /// Type of the share. + /// NetworkShareType. + /// Unknown share type + private NetworkShareType ToNetworkShareType(ShareType shareType) + { + switch (shareType) + { + case ShareType.Device: + return NetworkShareType.Device; + case ShareType.Disk : + return NetworkShareType.Disk; + case ShareType.IPC : + return NetworkShareType.Ipc; + case ShareType.Printer : + return NetworkShareType.Printer; + case ShareType.Special: + return NetworkShareType.Special; + default: + throw new ArgumentException("Unknown share type"); + } + } + + /// + /// Parses the specified endpointstring. + /// + /// The endpointstring. + /// IPEndPoint. + public IPEndPoint Parse(string endpointstring) + { + return Parse(endpointstring, -1); + } + + /// + /// Parses the specified endpointstring. + /// + /// The endpointstring. + /// The defaultport. + /// IPEndPoint. + /// Endpoint descriptor may not be empty. + /// + private static IPEndPoint Parse(string endpointstring, int defaultport) + { + if (string.IsNullOrEmpty(endpointstring) + || endpointstring.Trim().Length == 0) + { + throw new ArgumentException("Endpoint descriptor may not be empty."); + } + + if (defaultport != -1 && + (defaultport < IPEndPoint.MinPort + || defaultport > IPEndPoint.MaxPort)) + { + throw new ArgumentException(string.Format("Invalid default port '{0}'", defaultport)); + } + + string[] values = endpointstring.Split(new char[] { ':' }); + IPAddress ipaddy; + int port = -1; + + //check if we have an IPv6 or ports + if (values.Length <= 2) // ipv4 or hostname + { + if (values.Length == 1) + //no port is specified, default + port = defaultport; + else + port = GetPort(values[1]); + + //try to use the address as IPv4, otherwise get hostname + if (!IPAddress.TryParse(values[0], out ipaddy)) + ipaddy = GetIPfromHost(values[0]); + } + else if (values.Length > 2) //ipv6 + { + //could [a:b:c]:d + if (values[0].StartsWith("[") && values[values.Length - 2].EndsWith("]")) + { + string ipaddressstring = string.Join(":", values.Take(values.Length - 1).ToArray()); + ipaddy = IPAddress.Parse(ipaddressstring); + port = GetPort(values[values.Length - 1]); + } + else //[a:b:c] or a:b:c + { + ipaddy = IPAddress.Parse(endpointstring); + port = defaultport; + } + } + else + { + throw new FormatException(string.Format("Invalid endpoint ipaddress '{0}'", endpointstring)); + } + + if (port == -1) + throw new ArgumentException(string.Format("No port specified: '{0}'", endpointstring)); + + return new IPEndPoint(ipaddy, port); + } + + /// + /// Gets the port. + /// + /// The p. + /// System.Int32. + /// + private static int GetPort(string p) + { + int port; + + if (!int.TryParse(p, out port) + || port < IPEndPoint.MinPort + || port > IPEndPoint.MaxPort) + { + throw new FormatException(string.Format("Invalid end point port '{0}'", p)); + } + + return port; + } + + /// + /// Gets the I pfrom host. + /// + /// The p. + /// IPAddress. + /// + private static IPAddress GetIPfromHost(string p) + { + var hosts = Dns.GetHostAddresses(p); + + if (hosts == null || hosts.Length == 0) + throw new ArgumentException(string.Format("Host not found: {0}", p)); + + return hosts[0]; + } + } + +} diff --git a/MediaBrowser.Common.Implementations/NetworkManagement/NetworkShares.cs b/MediaBrowser.Common.Implementations/NetworkManagement/NetworkShares.cs new file mode 100644 index 0000000000..e2a8275cff --- /dev/null +++ b/MediaBrowser.Common.Implementations/NetworkManagement/NetworkShares.cs @@ -0,0 +1,638 @@ +using System; +using System.IO; +using System.Collections; +using System.Runtime.InteropServices; + +namespace MediaBrowser.Common.Implementations.NetworkManagement +{ + /// + /// 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$) + return ShareType.Special == _shareType && !string.IsNullOrEmpty(_netName); + } + } + + /// + /// Get the root of a disk-based share + /// + public DirectoryInfo Root + { + get + { + if (IsFileSystem) + { + if (string.IsNullOrEmpty(_server)) + if (string.IsNullOrEmpty(_path)) + return new DirectoryInfo(ToString()); + else + return new DirectoryInfo(_path); + return new DirectoryInfo(ToString()); + } + return null; + } + } + + #endregion + + /// + /// Returns the path to this share + /// + /// + public override string ToString() + { + if (string.IsNullOrEmpty(_server)) + { + return string.Format(@"\\{0}\{1}", Environment.MachineName, _netName); + } + 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 (string.IsNullOrEmpty(path)) 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.Implementations/README.txt b/MediaBrowser.Common.Implementations/README.txt new file mode 100644 index 0000000000..e8ce34205d --- /dev/null +++ b/MediaBrowser.Common.Implementations/README.txt @@ -0,0 +1,41 @@ +ServiceStack services should be available under '/api' path. If it's a brand new MVC project +install NuGet Package: ServiceStack.Host.Mvc. The package prepares ServiceStack default services. Make sure +that you added ignore for MVC routes: + + routes.IgnoreRoute("api/{*pathInfo}"); + +If it's MVC4 project, then don't forget to disable WebAPI: + + //WebApiConfig.Register(GlobalConfiguration.Configuration); + +Enable Swagger plugin in AppHost.cs with: + + public override void Configure(Container container) + { + ... + + Plugins.Add(new SwaggerFeature()); + // uncomment CORS feature if it's has to be available from external sites + //Plugins.Add(new CorsFeature()); + ... + + } + +Compile it. Now you can access swagger UI with: + +http://localost:port/swagger-ui/index.html + +or + +http://yoursite/swagger-ui/index.html + + +For more info about ServiceStack please visit: http://www.servicestack.net + +Feel free to ask questions about ServiceStack on: +http://stackoverflow.com/ + +or on the mailing Group at: +http://groups.google.com/group/servicestack + +Enjoy! \ No newline at end of file diff --git a/MediaBrowser.Common.Implementations/Server/RegisterServer.bat b/MediaBrowser.Common.Implementations/Server/RegisterServer.bat deleted file mode 100644 index d762dfaf76..0000000000 --- a/MediaBrowser.Common.Implementations/Server/RegisterServer.bat +++ /dev/null @@ -1,28 +0,0 @@ -rem %1 = http server port -rem %2 = http server url -rem %3 = udp server port -rem %4 = tcp server port (web socket) - -if [%1]==[] GOTO DONE - -netsh advfirewall firewall delete rule name="Port %1" protocol=TCP localport=%1 -netsh advfirewall firewall add rule name="Port %1" dir=in action=allow protocol=TCP localport=%1 - -if [%2]==[] GOTO DONE - -netsh http del urlacl url="%2" user="NT AUTHORITY\Authenticated Users" -netsh http add urlacl url="%2" user="NT AUTHORITY\Authenticated Users" - -if [%3]==[] GOTO DONE - -netsh advfirewall firewall delete rule name="Port %3" protocol=UDP localport=%3 -netsh advfirewall firewall add rule name="Port %3" dir=in action=allow protocol=UDP localport=%3 - -if [%4]==[] GOTO DONE - -netsh advfirewall firewall delete rule name="Port %4" protocol=TCP localport=%4 -netsh advfirewall firewall add rule name="Port %4" dir=in action=allow protocol=TCP localport=%4 - - -:DONE -Exit \ No newline at end of file diff --git a/MediaBrowser.Common.Implementations/Server/ServerManager.cs b/MediaBrowser.Common.Implementations/Server/ServerManager.cs deleted file mode 100644 index 04747bad6f..0000000000 --- a/MediaBrowser.Common.Implementations/Server/ServerManager.cs +++ /dev/null @@ -1,520 +0,0 @@ -using MediaBrowser.Common.Kernel; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Common.Implementations.Server -{ - /// - /// Manages the Http Server, Udp Server and WebSocket connections - /// - public class ServerManager : IServerManager, IDisposable - { - /// - /// This is the udp server used for server discovery by clients - /// - /// The UDP server. - private IUdpServer UdpServer { get; set; } - - /// - /// Both the Ui and server will have a built-in HttpServer. - /// People will inevitably want remote control apps so it's needed in the Ui too. - /// - /// The HTTP server. - private IHttpServer HttpServer { get; set; } - - /// - /// Gets or sets the json serializer. - /// - /// The json serializer. - private readonly IJsonSerializer _jsonSerializer; - - /// - /// This subscribes to HttpListener requests and finds the appropriate BaseHandler to process it - /// - /// The HTTP listener. - private IDisposable HttpListener { get; set; } - - /// - /// The web socket connections - /// - private readonly List _webSocketConnections = new List(); - - /// - /// Gets or sets the external web socket server. - /// - /// The external web socket server. - private IWebSocketServer ExternalWebSocketServer { get; set; } - - /// - /// The _logger - /// - private readonly ILogger _logger; - - /// - /// The _network manager - /// - private readonly INetworkManager _networkManager; - - /// - /// The _application host - /// - private readonly IApplicationHost _applicationHost; - - /// - /// The _kernel - /// - private readonly IKernel _kernel; - - /// - /// Gets a value indicating whether [supports web socket]. - /// - /// true if [supports web socket]; otherwise, false. - public bool SupportsNativeWebSocket - { - get { return HttpServer != null && HttpServer.SupportsWebSockets; } - } - - /// - /// Gets the web socket port number. - /// - /// The web socket port number. - public int WebSocketPortNumber - { - get { return SupportsNativeWebSocket ? _kernel.Configuration.HttpServerPortNumber : _kernel.Configuration.LegacyWebSocketPortNumber; } - } - - /// - /// Initializes a new instance of the class. - /// - /// The application host. - /// The kernel. - /// The network manager. - /// The json serializer. - /// The logger. - /// applicationHost - public ServerManager(IApplicationHost applicationHost, IKernel kernel, INetworkManager networkManager, IJsonSerializer jsonSerializer, ILogger logger) - { - if (applicationHost == null) - { - throw new ArgumentNullException("applicationHost"); - } - if (kernel == null) - { - throw new ArgumentNullException("kernel"); - } - if (networkManager == null) - { - throw new ArgumentNullException("networkManager"); - } - if (jsonSerializer == null) - { - throw new ArgumentNullException("jsonSerializer"); - } - if (logger == null) - { - throw new ArgumentNullException("logger"); - } - - _logger = logger; - _jsonSerializer = jsonSerializer; - _kernel = kernel; - _applicationHost = applicationHost; - _networkManager = networkManager; - } - - /// - /// Starts this instance. - /// - public void Start() - { - if (_applicationHost.IsFirstRun) - { - RegisterServerWithAdministratorAccess(); - } - - ReloadUdpServer(); - ReloadHttpServer(); - - if (!SupportsNativeWebSocket) - { - ReloadExternalWebSocketServer(); - } - - _kernel.ConfigurationUpdated += _kernel_ConfigurationUpdated; - } - - /// - /// Starts the external web socket server. - /// - private void ReloadExternalWebSocketServer() - { - // Avoid windows firewall prompts in the ui - if (_kernel.KernelContext != KernelContext.Server) - { - return; - } - - DisposeExternalWebSocketServer(); - - ExternalWebSocketServer = _applicationHost.Resolve(); - - ExternalWebSocketServer.Start(_kernel.Configuration.LegacyWebSocketPortNumber); - ExternalWebSocketServer.WebSocketConnected += HttpServer_WebSocketConnected; - } - - /// - /// Restarts the Http Server, or starts it if not currently running - /// - /// if set to true [register server on failure]. - private void ReloadHttpServer(bool registerServerOnFailure = true) - { - // Only reload if the port has changed, so that we don't disconnect any active users - if (HttpServer != null && HttpServer.UrlPrefix.Equals(_kernel.HttpServerUrlPrefix, StringComparison.OrdinalIgnoreCase)) - { - return; - } - - DisposeHttpServer(); - - _logger.Info("Loading Http Server"); - - try - { - HttpServer = _applicationHost.Resolve(); - HttpServer.EnableHttpRequestLogging = _kernel.Configuration.EnableHttpLevelLogging; - HttpServer.Start(_kernel.HttpServerUrlPrefix); - } - catch (HttpListenerException ex) - { - _logger.ErrorException("Error starting Http Server", ex); - - if (registerServerOnFailure) - { - RegisterServerWithAdministratorAccess(); - - // Don't get stuck in a loop - ReloadHttpServer(false); - - return; - } - - throw; - } - - HttpServer.WebSocketConnected += HttpServer_WebSocketConnected; - } - - /// - /// Handles the WebSocketConnected event of the HttpServer control. - /// - /// The source of the event. - /// The instance containing the event data. - void HttpServer_WebSocketConnected(object sender, WebSocketConnectEventArgs e) - { - var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) { OnReceive = ProcessWebSocketMessageReceived }; - - _webSocketConnections.Add(connection); - } - - /// - /// Processes the web socket message received. - /// - /// The result. - private async void ProcessWebSocketMessageReceived(WebSocketMessageInfo result) - { - var tasks = _kernel.WebSocketListeners.Select(i => Task.Run(async () => - { - try - { - await i.ProcessMessage(result).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("{0} failed processing WebSocket message {1}", ex, i.GetType().Name, result.MessageType); - } - })); - - await Task.WhenAll(tasks).ConfigureAwait(false); - } - - /// - /// Starts or re-starts the udp server - /// - private void ReloadUdpServer() - { - // For now, there's no reason to keep reloading this over and over - if (UdpServer != null) - { - return; - } - - // Avoid windows firewall prompts in the ui - if (_kernel.KernelContext != KernelContext.Server) - { - return; - } - - DisposeUdpServer(); - - try - { - // The port number can't be in configuration because we don't want it to ever change - UdpServer = _applicationHost.Resolve(); - UdpServer.Start(_kernel.UdpServerPortNumber); - } - catch (SocketException ex) - { - _logger.ErrorException("Failed to start UDP Server", ex); - return; - } - - UdpServer.MessageReceived += UdpServer_MessageReceived; - } - - /// - /// Handles the MessageReceived event of the UdpServer control. - /// - /// The source of the event. - /// The instance containing the event data. - async void UdpServer_MessageReceived(object sender, UdpMessageReceivedEventArgs e) - { - var expectedMessage = String.Format("who is MediaBrowser{0}?", _kernel.KernelContext); - var expectedMessageBytes = Encoding.UTF8.GetBytes(expectedMessage); - - if (expectedMessageBytes.SequenceEqual(e.Bytes)) - { - _logger.Info("Received UDP server request from " + e.RemoteEndPoint); - - // Send a response back with our ip address and port - var response = String.Format("MediaBrowser{0}|{1}:{2}", _kernel.KernelContext, _networkManager.GetLocalIpAddress(), _kernel.UdpServerPortNumber); - - await UdpServer.SendAsync(Encoding.UTF8.GetBytes(response), e.RemoteEndPoint); - } - } - - /// - /// Sends a message to all clients currently connected via a web socket - /// - /// - /// Type of the message. - /// The data. - /// Task. - public void SendWebSocketMessage(string messageType, T data) - { - SendWebSocketMessage(messageType, () => data); - } - - /// - /// Sends a message to all clients currently connected via a web socket - /// - /// - /// Type of the message. - /// The function that generates the data to send, if there are any connected clients - public void SendWebSocketMessage(string messageType, Func dataFunction) - { - Task.Run(async () => await SendWebSocketMessageAsync(messageType, dataFunction, CancellationToken.None).ConfigureAwait(false)); - } - - /// - /// Sends a message to all clients currently connected via a web socket - /// - /// - /// Type of the message. - /// The function that generates the data to send, if there are any connected clients - /// The cancellation token. - /// Task. - /// messageType - public async Task SendWebSocketMessageAsync(string messageType, Func dataFunction, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(messageType)) - { - throw new ArgumentNullException("messageType"); - } - - if (dataFunction == null) - { - throw new ArgumentNullException("dataFunction"); - } - - if (cancellationToken == null) - { - throw new ArgumentNullException("cancellationToken"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - var connections = _webSocketConnections.Where(s => s.State == WebSocketState.Open).ToList(); - - if (connections.Count > 0) - { - _logger.Info("Sending web socket message {0}", messageType); - - var message = new WebSocketMessage { MessageType = messageType, Data = dataFunction() }; - var bytes = _jsonSerializer.SerializeToBytes(message); - - var tasks = connections.Select(s => Task.Run(() => - { - try - { - s.SendAsync(bytes, cancellationToken); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error sending web socket message {0} to {1}", ex, messageType, s.RemoteEndPoint); - } - })); - - await Task.WhenAll(tasks).ConfigureAwait(false); - } - } - - /// - /// Disposes the udp server - /// - private void DisposeUdpServer() - { - if (UdpServer != null) - { - UdpServer.MessageReceived -= UdpServer_MessageReceived; - UdpServer.Dispose(); - } - } - - /// - /// Disposes the current HttpServer - /// - private void DisposeHttpServer() - { - foreach (var socket in _webSocketConnections) - { - // Dispose the connection - socket.Dispose(); - } - - _webSocketConnections.Clear(); - - if (HttpServer != null) - { - _logger.Info("Disposing Http Server"); - - HttpServer.WebSocketConnected -= HttpServer_WebSocketConnected; - HttpServer.Dispose(); - } - - if (HttpListener != null) - { - HttpListener.Dispose(); - } - - DisposeExternalWebSocketServer(); - } - - /// - /// Registers the server with administrator access. - /// - private void RegisterServerWithAdministratorAccess() - { - // Create a temp file path to extract the bat file to - var tmpFile = Path.Combine(_kernel.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".bat"); - - // Extract the bat file - using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MediaBrowser.Common.Implementations.Server.bat")) - { - using (var fileStream = File.Create(tmpFile)) - { - stream.CopyTo(fileStream); - } - } - - var startInfo = new ProcessStartInfo - { - FileName = tmpFile, - - Arguments = string.Format("{0} {1} {2} {3}", _kernel.Configuration.HttpServerPortNumber, - _kernel.HttpServerUrlPrefix, - _kernel.UdpServerPortNumber, - _kernel.Configuration.LegacyWebSocketPortNumber), - - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - Verb = "runas", - ErrorDialog = false - }; - - using (var process = Process.Start(startInfo)) - { - process.WaitForExit(); - } - } - - /// - /// 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) - { - DisposeUdpServer(); - DisposeHttpServer(); - } - } - - /// - /// Disposes the external web socket server. - /// - private void DisposeExternalWebSocketServer() - { - if (ExternalWebSocketServer != null) - { - ExternalWebSocketServer.Dispose(); - } - } - - /// - /// Handles the ConfigurationUpdated event of the _kernel control. - /// - /// The source of the event. - /// The instance containing the event data. - /// - void _kernel_ConfigurationUpdated(object sender, EventArgs e) - { - HttpServer.EnableHttpRequestLogging = _kernel.Configuration.EnableHttpLevelLogging; - - if (!string.Equals(HttpServer.UrlPrefix, _kernel.HttpServerUrlPrefix, StringComparison.OrdinalIgnoreCase)) - { - ReloadHttpServer(); - } - - if (!SupportsNativeWebSocket && ExternalWebSocketServer != null && ExternalWebSocketServer.Port != _kernel.Configuration.LegacyWebSocketPortNumber) - { - ReloadExternalWebSocketServer(); - } - } - } -} diff --git a/MediaBrowser.Common.Implementations/Server/WebSocketConnection.cs b/MediaBrowser.Common.Implementations/Server/WebSocketConnection.cs deleted file mode 100644 index b8766523cd..0000000000 --- a/MediaBrowser.Common.Implementations/Server/WebSocketConnection.cs +++ /dev/null @@ -1,225 +0,0 @@ -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Common.Implementations.Server -{ - /// - /// Class WebSocketConnection - /// - public class WebSocketConnection : IWebSocketConnection - { - /// - /// The _socket - /// - private readonly IWebSocket _socket; - - /// - /// The _remote end point - /// - public string RemoteEndPoint { get; private set; } - - /// - /// The _cancellation token source - /// - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - /// - /// The _send semaphore - /// - private readonly SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1,1); - - /// - /// The logger - /// - private readonly ILogger _logger; - - /// - /// The _json serializer - /// - private readonly IJsonSerializer _jsonSerializer; - - /// - /// Gets or sets the receive action. - /// - /// The receive action. - public Action OnReceive { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The socket. - /// The remote end point. - /// The json serializer. - /// The logger. - /// socket - public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger) - { - if (socket == null) - { - throw new ArgumentNullException("socket"); - } - if (string.IsNullOrEmpty(remoteEndPoint)) - { - throw new ArgumentNullException("remoteEndPoint"); - } - if (jsonSerializer == null) - { - throw new ArgumentNullException("jsonSerializer"); - } - if (logger == null) - { - throw new ArgumentNullException("logger"); - } - - _jsonSerializer = jsonSerializer; - _socket = socket; - _socket.OnReceiveDelegate = OnReceiveInternal; - RemoteEndPoint = remoteEndPoint; - _logger = logger; - } - - /// - /// Called when [receive]. - /// - /// The bytes. - private void OnReceiveInternal(byte[] bytes) - { - if (OnReceive == null) - { - return; - } - try - { - WebSocketMessageInfo info; - - using (var memoryStream = new MemoryStream(bytes)) - { - info = (WebSocketMessageInfo)_jsonSerializer.DeserializeFromStream(memoryStream, typeof(WebSocketMessageInfo)); - } - - info.Connection = this; - - OnReceive(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(); - } - } - } -} diff --git a/MediaBrowser.Common.Implementations/ServerManager/RegisterServer.bat b/MediaBrowser.Common.Implementations/ServerManager/RegisterServer.bat new file mode 100644 index 0000000000..d762dfaf76 --- /dev/null +++ b/MediaBrowser.Common.Implementations/ServerManager/RegisterServer.bat @@ -0,0 +1,28 @@ +rem %1 = http server port +rem %2 = http server url +rem %3 = udp server port +rem %4 = tcp server port (web socket) + +if [%1]==[] GOTO DONE + +netsh advfirewall firewall delete rule name="Port %1" protocol=TCP localport=%1 +netsh advfirewall firewall add rule name="Port %1" dir=in action=allow protocol=TCP localport=%1 + +if [%2]==[] GOTO DONE + +netsh http del urlacl url="%2" user="NT AUTHORITY\Authenticated Users" +netsh http add urlacl url="%2" user="NT AUTHORITY\Authenticated Users" + +if [%3]==[] GOTO DONE + +netsh advfirewall firewall delete rule name="Port %3" protocol=UDP localport=%3 +netsh advfirewall firewall add rule name="Port %3" dir=in action=allow protocol=UDP localport=%3 + +if [%4]==[] GOTO DONE + +netsh advfirewall firewall delete rule name="Port %4" protocol=TCP localport=%4 +netsh advfirewall firewall add rule name="Port %4" dir=in action=allow protocol=TCP localport=%4 + + +:DONE +Exit \ No newline at end of file diff --git a/MediaBrowser.Common.Implementations/ServerManager/ServerManager.cs b/MediaBrowser.Common.Implementations/ServerManager/ServerManager.cs new file mode 100644 index 0000000000..2a3b0155f3 --- /dev/null +++ b/MediaBrowser.Common.Implementations/ServerManager/ServerManager.cs @@ -0,0 +1,520 @@ +using MediaBrowser.Common.Kernel; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Implementations.ServerManager +{ + /// + /// Manages the Http Server, Udp Server and WebSocket connections + /// + public class ServerManager : IServerManager, IDisposable + { + /// + /// This is the udp server used for server discovery by clients + /// + /// The UDP server. + private IUdpServer UdpServer { get; set; } + + /// + /// Both the Ui and server will have a built-in HttpServer. + /// People will inevitably want remote control apps so it's needed in the Ui too. + /// + /// The HTTP server. + private IHttpServer HttpServer { get; set; } + + /// + /// Gets or sets the json serializer. + /// + /// The json serializer. + private readonly IJsonSerializer _jsonSerializer; + + /// + /// This subscribes to HttpListener requests and finds the appropriate BaseHandler to process it + /// + /// The HTTP listener. + private IDisposable HttpListener { get; set; } + + /// + /// The web socket connections + /// + private readonly List _webSocketConnections = new List(); + + /// + /// Gets or sets the external web socket server. + /// + /// The external web socket server. + private IWebSocketServer ExternalWebSocketServer { get; set; } + + /// + /// The _logger + /// + private readonly ILogger _logger; + + /// + /// The _network manager + /// + private readonly INetworkManager _networkManager; + + /// + /// The _application host + /// + private readonly IApplicationHost _applicationHost; + + /// + /// The _kernel + /// + private readonly IKernel _kernel; + + /// + /// Gets a value indicating whether [supports web socket]. + /// + /// true if [supports web socket]; otherwise, false. + public bool SupportsNativeWebSocket + { + get { return HttpServer != null && HttpServer.SupportsWebSockets; } + } + + /// + /// Gets the web socket port number. + /// + /// The web socket port number. + public int WebSocketPortNumber + { + get { return SupportsNativeWebSocket ? _kernel.Configuration.HttpServerPortNumber : _kernel.Configuration.LegacyWebSocketPortNumber; } + } + + /// + /// Initializes a new instance of the class. + /// + /// The application host. + /// The kernel. + /// The network manager. + /// The json serializer. + /// The logger. + /// applicationHost + public ServerManager(IApplicationHost applicationHost, IKernel kernel, INetworkManager networkManager, IJsonSerializer jsonSerializer, ILogger logger) + { + if (applicationHost == null) + { + throw new ArgumentNullException("applicationHost"); + } + if (kernel == null) + { + throw new ArgumentNullException("kernel"); + } + if (networkManager == null) + { + throw new ArgumentNullException("networkManager"); + } + if (jsonSerializer == null) + { + throw new ArgumentNullException("jsonSerializer"); + } + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + _logger = logger; + _jsonSerializer = jsonSerializer; + _kernel = kernel; + _applicationHost = applicationHost; + _networkManager = networkManager; + } + + /// + /// Starts this instance. + /// + public void Start() + { + if (_applicationHost.IsFirstRun) + { + RegisterServerWithAdministratorAccess(); + } + + ReloadUdpServer(); + ReloadHttpServer(); + + if (!SupportsNativeWebSocket) + { + ReloadExternalWebSocketServer(); + } + + _kernel.ConfigurationUpdated += _kernel_ConfigurationUpdated; + } + + /// + /// Starts the external web socket server. + /// + private void ReloadExternalWebSocketServer() + { + // Avoid windows firewall prompts in the ui + if (_kernel.KernelContext != KernelContext.Server) + { + return; + } + + DisposeExternalWebSocketServer(); + + ExternalWebSocketServer = _applicationHost.Resolve(); + + ExternalWebSocketServer.Start(_kernel.Configuration.LegacyWebSocketPortNumber); + ExternalWebSocketServer.WebSocketConnected += HttpServer_WebSocketConnected; + } + + /// + /// Restarts the Http Server, or starts it if not currently running + /// + /// if set to true [register server on failure]. + private void ReloadHttpServer(bool registerServerOnFailure = true) + { + // Only reload if the port has changed, so that we don't disconnect any active users + if (HttpServer != null && HttpServer.UrlPrefix.Equals(_kernel.HttpServerUrlPrefix, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + DisposeHttpServer(); + + _logger.Info("Loading Http Server"); + + try + { + HttpServer = _applicationHost.Resolve(); + HttpServer.EnableHttpRequestLogging = _kernel.Configuration.EnableHttpLevelLogging; + HttpServer.Start(_kernel.HttpServerUrlPrefix); + } + catch (HttpListenerException ex) + { + _logger.ErrorException("Error starting Http Server", ex); + + if (registerServerOnFailure) + { + RegisterServerWithAdministratorAccess(); + + // Don't get stuck in a loop + ReloadHttpServer(false); + + return; + } + + throw; + } + + HttpServer.WebSocketConnected += HttpServer_WebSocketConnected; + } + + /// + /// Handles the WebSocketConnected event of the HttpServer control. + /// + /// The source of the event. + /// The instance containing the event data. + void HttpServer_WebSocketConnected(object sender, WebSocketConnectEventArgs e) + { + var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) { OnReceive = ProcessWebSocketMessageReceived }; + + _webSocketConnections.Add(connection); + } + + /// + /// Processes the web socket message received. + /// + /// The result. + private async void ProcessWebSocketMessageReceived(WebSocketMessageInfo result) + { + var tasks = _kernel.WebSocketListeners.Select(i => Task.Run(async () => + { + try + { + await i.ProcessMessage(result).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("{0} failed processing WebSocket message {1}", ex, i.GetType().Name, result.MessageType); + } + })); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + /// + /// Starts or re-starts the udp server + /// + private void ReloadUdpServer() + { + // For now, there's no reason to keep reloading this over and over + if (UdpServer != null) + { + return; + } + + // Avoid windows firewall prompts in the ui + if (_kernel.KernelContext != KernelContext.Server) + { + return; + } + + DisposeUdpServer(); + + try + { + // The port number can't be in configuration because we don't want it to ever change + UdpServer = _applicationHost.Resolve(); + UdpServer.Start(_kernel.UdpServerPortNumber); + } + catch (SocketException ex) + { + _logger.ErrorException("Failed to start UDP Server", ex); + return; + } + + UdpServer.MessageReceived += UdpServer_MessageReceived; + } + + /// + /// Handles the MessageReceived event of the UdpServer control. + /// + /// The source of the event. + /// The instance containing the event data. + async void UdpServer_MessageReceived(object sender, UdpMessageReceivedEventArgs e) + { + var expectedMessage = String.Format("who is MediaBrowser{0}?", _kernel.KernelContext); + var expectedMessageBytes = Encoding.UTF8.GetBytes(expectedMessage); + + if (expectedMessageBytes.SequenceEqual(e.Bytes)) + { + _logger.Info("Received UDP server request from " + e.RemoteEndPoint); + + // Send a response back with our ip address and port + var response = String.Format("MediaBrowser{0}|{1}:{2}", _kernel.KernelContext, _networkManager.GetLocalIpAddress(), _kernel.UdpServerPortNumber); + + await UdpServer.SendAsync(Encoding.UTF8.GetBytes(response), e.RemoteEndPoint); + } + } + + /// + /// Sends a message to all clients currently connected via a web socket + /// + /// + /// Type of the message. + /// The data. + /// Task. + public void SendWebSocketMessage(string messageType, T data) + { + SendWebSocketMessage(messageType, () => data); + } + + /// + /// Sends a message to all clients currently connected via a web socket + /// + /// + /// Type of the message. + /// The function that generates the data to send, if there are any connected clients + public void SendWebSocketMessage(string messageType, Func dataFunction) + { + Task.Run(async () => await SendWebSocketMessageAsync(messageType, dataFunction, CancellationToken.None).ConfigureAwait(false)); + } + + /// + /// Sends a message to all clients currently connected via a web socket + /// + /// + /// Type of the message. + /// The function that generates the data to send, if there are any connected clients + /// The cancellation token. + /// Task. + /// messageType + public async Task SendWebSocketMessageAsync(string messageType, Func dataFunction, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(messageType)) + { + throw new ArgumentNullException("messageType"); + } + + if (dataFunction == null) + { + throw new ArgumentNullException("dataFunction"); + } + + if (cancellationToken == null) + { + throw new ArgumentNullException("cancellationToken"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var connections = _webSocketConnections.Where(s => s.State == WebSocketState.Open).ToList(); + + if (connections.Count > 0) + { + _logger.Info("Sending web socket message {0}", messageType); + + var message = new WebSocketMessage { MessageType = messageType, Data = dataFunction() }; + var bytes = _jsonSerializer.SerializeToBytes(message); + + var tasks = connections.Select(s => Task.Run(() => + { + try + { + s.SendAsync(bytes, cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error sending web socket message {0} to {1}", ex, messageType, s.RemoteEndPoint); + } + })); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + } + + /// + /// Disposes the udp server + /// + private void DisposeUdpServer() + { + if (UdpServer != null) + { + UdpServer.MessageReceived -= UdpServer_MessageReceived; + UdpServer.Dispose(); + } + } + + /// + /// Disposes the current HttpServer + /// + private void DisposeHttpServer() + { + foreach (var socket in _webSocketConnections) + { + // Dispose the connection + socket.Dispose(); + } + + _webSocketConnections.Clear(); + + if (HttpServer != null) + { + _logger.Info("Disposing Http Server"); + + HttpServer.WebSocketConnected -= HttpServer_WebSocketConnected; + HttpServer.Dispose(); + } + + if (HttpListener != null) + { + HttpListener.Dispose(); + } + + DisposeExternalWebSocketServer(); + } + + /// + /// Registers the server with administrator access. + /// + private void RegisterServerWithAdministratorAccess() + { + // Create a temp file path to extract the bat file to + var tmpFile = Path.Combine(_kernel.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".bat"); + + // Extract the bat file + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MediaBrowser.Common.Implementations.ServerManager.RegisterServer.bat")) + { + using (var fileStream = File.Create(tmpFile)) + { + stream.CopyTo(fileStream); + } + } + + var startInfo = new ProcessStartInfo + { + FileName = tmpFile, + + Arguments = string.Format("{0} {1} {2} {3}", _kernel.Configuration.HttpServerPortNumber, + _kernel.HttpServerUrlPrefix, + _kernel.UdpServerPortNumber, + _kernel.Configuration.LegacyWebSocketPortNumber), + + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + Verb = "runas", + ErrorDialog = false + }; + + using (var process = Process.Start(startInfo)) + { + process.WaitForExit(); + } + } + + /// + /// 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) + { + DisposeUdpServer(); + DisposeHttpServer(); + } + } + + /// + /// Disposes the external web socket server. + /// + private void DisposeExternalWebSocketServer() + { + if (ExternalWebSocketServer != null) + { + ExternalWebSocketServer.Dispose(); + } + } + + /// + /// Handles the ConfigurationUpdated event of the _kernel control. + /// + /// The source of the event. + /// The instance containing the event data. + /// + void _kernel_ConfigurationUpdated(object sender, EventArgs e) + { + HttpServer.EnableHttpRequestLogging = _kernel.Configuration.EnableHttpLevelLogging; + + if (!string.Equals(HttpServer.UrlPrefix, _kernel.HttpServerUrlPrefix, StringComparison.OrdinalIgnoreCase)) + { + ReloadHttpServer(); + } + + if (!SupportsNativeWebSocket && ExternalWebSocketServer != null && ExternalWebSocketServer.Port != _kernel.Configuration.LegacyWebSocketPortNumber) + { + ReloadExternalWebSocketServer(); + } + } + } +} diff --git a/MediaBrowser.Common.Implementations/ServerManager/WebSocketConnection.cs b/MediaBrowser.Common.Implementations/ServerManager/WebSocketConnection.cs new file mode 100644 index 0000000000..f826b02cc1 --- /dev/null +++ b/MediaBrowser.Common.Implementations/ServerManager/WebSocketConnection.cs @@ -0,0 +1,225 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Implementations.ServerManager +{ + /// + /// Class WebSocketConnection + /// + public class WebSocketConnection : IWebSocketConnection + { + /// + /// The _socket + /// + private readonly IWebSocket _socket; + + /// + /// The _remote end point + /// + public string RemoteEndPoint { get; private set; } + + /// + /// The _cancellation token source + /// + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + + /// + /// The _send semaphore + /// + private readonly SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1,1); + + /// + /// The logger + /// + private readonly ILogger _logger; + + /// + /// The _json serializer + /// + private readonly IJsonSerializer _jsonSerializer; + + /// + /// Gets or sets the receive action. + /// + /// The receive action. + public Action OnReceive { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The socket. + /// The remote end point. + /// The json serializer. + /// The logger. + /// socket + public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger) + { + if (socket == null) + { + throw new ArgumentNullException("socket"); + } + if (string.IsNullOrEmpty(remoteEndPoint)) + { + throw new ArgumentNullException("remoteEndPoint"); + } + if (jsonSerializer == null) + { + throw new ArgumentNullException("jsonSerializer"); + } + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + _jsonSerializer = jsonSerializer; + _socket = socket; + _socket.OnReceiveDelegate = OnReceiveInternal; + RemoteEndPoint = remoteEndPoint; + _logger = logger; + } + + /// + /// Called when [receive]. + /// + /// The bytes. + private void OnReceiveInternal(byte[] bytes) + { + if (OnReceive == null) + { + return; + } + try + { + WebSocketMessageInfo info; + + using (var memoryStream = new MemoryStream(bytes)) + { + info = (WebSocketMessageInfo)_jsonSerializer.DeserializeFromStream(memoryStream, typeof(WebSocketMessageInfo)); + } + + info.Connection = this; + + OnReceive(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(); + } + } + } +} diff --git a/MediaBrowser.Common.Implementations/Udp/UdpServer.cs b/MediaBrowser.Common.Implementations/Udp/UdpServer.cs new file mode 100644 index 0000000000..e0435ab3cb --- /dev/null +++ b/MediaBrowser.Common.Implementations/Udp/UdpServer.cs @@ -0,0 +1,203 @@ +using MediaBrowser.Common.Implementations.NetworkManagement; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using System; +using System.Net; +using System.Net.Sockets; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Implementations.Udp +{ + /// + /// Provides a Udp Server + /// + public class UdpServer : IUdpServer + { + /// + /// Occurs when [message received]. + /// + public event EventHandler MessageReceived; + + /// + /// Gets or sets the logger. + /// + /// The logger. + private ILogger Logger { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public UdpServer(ILogger logger) + { + Logger = logger; + } + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected virtual void OnMessageReceived(UdpMessageReceivedEventArgs e) + { + EventHandler handler = MessageReceived; + if (handler != null) handler(this, e); + } + + /// + /// The _udp client + /// + private UdpClient _udpClient; + + /// + /// Starts the specified port. + /// + /// The port. + public void Start(int port) + { + _udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, port)); + + _udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + + CreateObservable().Subscribe(OnMessageReceived); + } + + /// + /// Creates the observable. + /// + /// IObservable{UdpReceiveResult}. + private IObservable CreateObservable() + { + return Observable.Create(obs => + Observable.FromAsync(() => + { + try + { + return _udpClient.ReceiveAsync(); + } + catch (ObjectDisposedException) + { + return Task.FromResult(new UdpReceiveResult(new byte[]{}, new IPEndPoint(IPAddress.Any, 0))); + } + catch (Exception ex) + { + Logger.ErrorException("Error receiving udp message", ex); + return Task.FromResult(new UdpReceiveResult(new byte[] { }, new IPEndPoint(IPAddress.Any, 0))); + } + }) + + .Subscribe(obs)) + .Repeat() + .Retry() + .Publish() + .RefCount(); + } + + /// + /// Called when [message received]. + /// + /// The message. + private void OnMessageReceived(UdpReceiveResult message) + { + if (message.RemoteEndPoint.Port == 0) + { + return; + } + var bytes = message.Buffer; + + OnMessageReceived(new UdpMessageReceivedEventArgs + { + Bytes = bytes, + RemoteEndPoint = message.RemoteEndPoint.ToString() + }); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Stops this instance. + /// + public void Stop() + { + _udpClient.Close(); + } + + /// + /// 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) + { + Stop(); + } + } + + /// + /// Sends the async. + /// + /// The data. + /// The ip address. + /// The port. + /// Task{System.Int32}. + /// data + public Task SendAsync(string data, string ipAddress, int port) + { + return SendAsync(Encoding.UTF8.GetBytes(data), ipAddress, port); + } + + /// + /// Sends the async. + /// + /// The bytes. + /// The ip address. + /// The port. + /// Task{System.Int32}. + /// bytes + public Task SendAsync(byte[] bytes, string ipAddress, int port) + { + if (bytes == null) + { + throw new ArgumentNullException("bytes"); + } + + if (string.IsNullOrEmpty(ipAddress)) + { + throw new ArgumentNullException("ipAddress"); + } + + return _udpClient.SendAsync(bytes, bytes.Length, ipAddress, port); + } + + /// + /// Sends the async. + /// + /// The bytes. + /// The remote end point. + /// Task. + public Task SendAsync(byte[] bytes, string remoteEndPoint) + { + if (bytes == null) + { + throw new ArgumentNullException("bytes"); + } + + if (string.IsNullOrEmpty(remoteEndPoint)) + { + throw new ArgumentNullException("remoteEndPoint"); + } + + return _udpClient.SendAsync(bytes, bytes.Length, new NetworkManager().Parse(remoteEndPoint)); + } + } + +} diff --git a/MediaBrowser.Common.Implementations/WebSocket/AlchemyServer.cs b/MediaBrowser.Common.Implementations/WebSocket/AlchemyServer.cs new file mode 100644 index 0000000000..236bd27b7b --- /dev/null +++ b/MediaBrowser.Common.Implementations/WebSocket/AlchemyServer.cs @@ -0,0 +1,113 @@ +using Alchemy; +using Alchemy.Classes; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using System; +using System.Net; + +namespace MediaBrowser.Common.Implementations.WebSocket +{ + /// + /// Class AlchemyServer + /// + public class AlchemyServer : IWebSocketServer + { + /// + /// Occurs when [web socket connected]. + /// + public event EventHandler WebSocketConnected; + + /// + /// Gets or sets the web socket server. + /// + /// The web socket server. + private WebSocketServer WebSocketServer { get; set; } + + /// + /// The _logger + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// logger + public AlchemyServer(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + _logger = logger; + } + + /// + /// Gets the port. + /// + /// The port. + public int Port { get; private set; } + + /// + /// Starts the specified port number. + /// + /// The port number. + public void Start(int portNumber) + { + WebSocketServer = new WebSocketServer(portNumber, IPAddress.Any) + { + OnConnected = OnAlchemyWebSocketClientConnected, + TimeOut = TimeSpan.FromMinutes(60) + }; + + WebSocketServer.Start(); + + Port = portNumber; + + _logger.Info("Alchemy Web Socket Server started"); + } + + /// + /// Called when [alchemy web socket client connected]. + /// + /// The context. + private void OnAlchemyWebSocketClientConnected(UserContext context) + { + if (WebSocketConnected != null) + { + var socket = new AlchemyWebSocket(context, _logger); + + WebSocketConnected(this, new WebSocketConnectEventArgs + { + WebSocket = socket, + Endpoint = context.ClientAddress.ToString() + }); + } + } + + /// + /// Stops this instance. + /// + public void Stop() + { + } + + /// + /// 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) + { + + } + } +} diff --git a/MediaBrowser.Common.Implementations/WebSocket/AlchemyWebSocket.cs b/MediaBrowser.Common.Implementations/WebSocket/AlchemyWebSocket.cs new file mode 100644 index 0000000000..1ecc38f2c2 --- /dev/null +++ b/MediaBrowser.Common.Implementations/WebSocket/AlchemyWebSocket.cs @@ -0,0 +1,132 @@ +using Alchemy.Classes; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Implementations.WebSocket +{ + /// + /// Class AlchemyWebSocket + /// + public class AlchemyWebSocket : IWebSocket + { + /// + /// The logger + /// + private readonly ILogger _logger; + + /// + /// Gets or sets the web socket. + /// + /// The web socket. + private UserContext UserContext { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The logger. + /// context + public AlchemyWebSocket(UserContext context, ILogger logger) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + _logger = logger; + 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 bytes = Encoding.UTF8.GetBytes(json); + + OnReceiveDelegate(bytes); + } + 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.Implementations/packages.config b/MediaBrowser.Common.Implementations/packages.config index 2578e48f3b..ac80a0a03b 100644 --- a/MediaBrowser.Common.Implementations/packages.config +++ b/MediaBrowser.Common.Implementations/packages.config @@ -1,7 +1,17 @@  + + + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Common.Implementations/swagger-ui/css/screen.css b/MediaBrowser.Common.Implementations/swagger-ui/css/screen.css new file mode 100644 index 0000000000..07773f9efe --- /dev/null +++ b/MediaBrowser.Common.Implementations/swagger-ui/css/screen.css @@ -0,0 +1,959 @@ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; } + +body { + line-height: 1; } + +ol, ul { + list-style: none; } + +table { + border-collapse: collapse; + border-spacing: 0; } + +caption, th, td { + text-align: left; + font-weight: normal; + vertical-align: middle; } + +q, blockquote { + quotes: none; } + q:before, q:after, blockquote:before, blockquote:after { + content: ""; + content: none; } + +a img { + border: none; } + +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary { + display: block; } + +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + text-decoration: none; } + h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover, h5 a:hover, h6 a:hover { + text-decoration: underline; } +h1 span.divider, h2 span.divider, h3 span.divider, h4 span.divider, h5 span.divider, h6 span.divider { + color: #aaaaaa; } + +h1 { + color: #547f00; + color: black; + font-size: 1.5em; + line-height: 1.3em; + padding: 10px 0 10px 0; + font-family: "Droid Sans", sans-serif; + font-weight: bold; } + +h2 { + color: #89bf04; + color: black; + font-size: 1.3em; + padding: 10px 0 10px 0; } + h2 a { + color: black; } + h2 span.sub { + font-size: 0.7em; + color: #999999; + font-style: italic; } + h2 span.sub a { + color: #777777; } + +h3 { + color: black; + font-size: 1.1em; + padding: 10px 0 10px 0; } + +div.heading_with_menu { + float: none; + clear: both; + overflow: hidden; + display: block; } + div.heading_with_menu h1, div.heading_with_menu h2, div.heading_with_menu h3, div.heading_with_menu h4, div.heading_with_menu h5, div.heading_with_menu h6 { + display: block; + clear: none; + float: left; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + width: 60%; } + div.heading_with_menu ul { + display: block; + clear: none; + float: right; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + margin-top: 10px; } + +.body-textarea { + width: 300px; + height: 100px; +} + +p { + line-height: 1.4em; + padding: 0 0 10px 0; + color: #333333; } + +ol { + margin: 0px 0 10px 0; + padding: 0 0 0 18px; + list-style-type: decimal; } + ol li { + padding: 5px 0px; + font-size: 0.9em; + color: #333333; } + +.markdown h3 { + color: #547f00; } +.markdown h4 { + color: #666666; } +.markdown pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; + padding: 10px; + margin: 0 0 10px 0; } + .markdown pre code { + line-height: 1.6em; } +.markdown p code, .markdown li code { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #f0f0f0; + color: black; + padding: 1px 3px; } +.markdown ol, .markdown ul { + font-family: "Droid Sans", sans-serif; + margin: 5px 0 10px 0; + padding: 0 0 0 18px; + list-style-type: disc; } + .markdown ol li, .markdown ul li { + padding: 3px 0px; + line-height: 1.4em; + color: #333333; } + +div.gist { + margin: 20px 0 25px 0 !important; } + +p.big, div.big p { + font-size: 1 em; + margin-bottom: 10px; } + +span.weak { + color: #666666; } +span.blank, span.empty { + color: #888888; + font-style: italic; } + +a { + color: #547f00; } + +strong { + font-family: "Droid Sans", sans-serif; + font-weight: bold; + font-weight: bold; } + +.code { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; } + +pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; + padding: 10px; + /* white-space: pre-line */ } + pre code { + line-height: 1.6em; } + +.required { + font-weight: bold; } + +table.fullwidth { + width: 100%; } +table thead tr th { + padding: 5px; + font-size: 0.9em; + color: #666666; + border-bottom: 1px solid #999999; } +table tbody tr.offset { + background-color: #f5f5f5; } +table tbody tr td { + padding: 6px; + font-size: 0.9em; + border-bottom: 1px solid #cccccc; + vertical-align: top; + line-height: 1.3em; } +table tbody tr:last-child td { + border-bottom: none; } +table tbody tr.offset { + background-color: #f0f0f0; } + +form.form_box { + background-color: #ebf3f9; + border: 1px solid black; + border-color: #c3d9ec; + padding: 10px; } + form.form_box label { + color: #0f6ab4 !important; } + form.form_box input[type=submit] { + display: block; + padding: 10px; } + form.form_box p { + font-size: 0.9em; + padding: 0 0 15px 0; + color: #7e7b6d; } + form.form_box p a { + color: #646257; } + form.form_box p strong { + color: black; } + form.form_box p.weak { + font-size: 0.8em; } +form.formtastic fieldset.inputs ol li p.inline-hints { + margin-left: 0; + font-style: italic; + font-size: 0.9em; + margin: 0; } +form.formtastic fieldset.inputs ol li label { + display: block; + clear: both; + width: auto; + padding: 0 0 3px 0; + color: #666666; } + form.formtastic fieldset.inputs ol li label abbr { + padding-left: 3px; + color: #888888; } +form.formtastic fieldset.inputs ol li.required label { + color: black; } +form.formtastic fieldset.inputs ol li.string input, form.formtastic fieldset.inputs ol li.url input, form.formtastic fieldset.inputs ol li.numeric input { + display: block; + padding: 4px; + width: auto; + clear: both; } + form.formtastic fieldset.inputs ol li.string input.title, form.formtastic fieldset.inputs ol li.url input.title, form.formtastic fieldset.inputs ol li.numeric input.title { + font-size: 1.3em; } +form.formtastic fieldset.inputs ol li.text textarea { + font-family: "Droid Sans", sans-serif; + height: 250px; + padding: 4px; + display: block; + clear: both; } +form.formtastic fieldset.inputs ol li.select select { + display: block; + clear: both; } +form.formtastic fieldset.inputs ol li.boolean { + float: none; + clear: both; + overflow: hidden; + display: block; } + form.formtastic fieldset.inputs ol li.boolean input { + display: block; + float: left; + clear: none; + margin: 0 5px 0 0; } + form.formtastic fieldset.inputs ol li.boolean label { + display: block; + float: left; + clear: none; + margin: 0; + padding: 0; } +form.formtastic fieldset.buttons { + margin: 0; + padding: 0; } +form.fullwidth ol li.string input, form.fullwidth ol li.url input, form.fullwidth ol li.text textarea, form.fullwidth ol li.numeric input { + width: 500px !important; } + +body { + font-family: "Droid Sans", sans-serif; } + body #content_message { + margin: 10px 15px; + font-style: italic; + color: #999999; } + body #header { + background-color: #89bf04; + padding: 14px; } + body #header a#logo { + font-size: 1.5em; + font-weight: bold; + text-decoration: none; + background: transparent url(http://swagger.wordnik.com/images/logo_small.png) no-repeat left center; + padding: 20px 0 20px 40px; + color: white; } + body #header form#api_selector { + display: block; + clear: none; + float: right; } + body #header form#api_selector .input { + display: block; + clear: none; + float: left; + margin: 0 10px 0 0; } + body #header form#api_selector .input input { + font-size: 0.9em; + padding: 3px; + margin: 0; } + body #header form#api_selector .input input#input_baseUrl { + width: 400px; } + body #header form#api_selector .input input#input_apiKey { + width: 200px; } + body #header form#api_selector .input a#explore { + display: block; + text-decoration: none; + font-weight: bold; + padding: 6px 8px; + font-size: 0.9em; + color: white; + background-color: #547f00; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -o-border-radius: 4px; + -ms-border-radius: 4px; + -khtml-border-radius: 4px; + border-radius: 4px; } + body #header form#api_selector .input a#explore:hover { + background-color: #547f00; } + body p#colophon { + margin: 0 15px 40px 15px; + padding: 10px 0; + font-size: 0.8em; + border-top: 1px solid #dddddd; + font-family: "Droid Sans", sans-serif; + color: #999999; + font-style: italic; } + body p#colophon a { + text-decoration: none; + color: #547f00; } + body ul#resources { + font-family: "Droid Sans", sans-serif; + font-size: 0.9em; } + body ul#resources li.resource { + border-bottom: 1px solid #dddddd; } + body ul#resources li.resource:last-child { + border-bottom: none; } + body ul#resources li.resource div.heading { + border: 1px solid transparent; + float: none; + clear: both; + overflow: hidden; + display: block; } + body ul#resources li.resource div.heading h2 { + color: #999999; + padding-left: 0px; + display: block; + clear: none; + float: left; + font-family: "Droid Sans", sans-serif; + font-weight: bold; } + body ul#resources li.resource div.heading h2 a { + color: #999999; } + body ul#resources li.resource div.heading h2 a:hover { + color: black; } + body ul#resources li.resource div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 14px 10px 0 0; } + body ul#resources li.resource div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; } + body ul#resources li.resource div.heading ul.options li:first-child, body ul#resources li.resource div.heading ul.options li.first { + padding-left: 0; } + body ul#resources li.resource div.heading ul.options li:last-child, body ul#resources li.resource div.heading ul.options li.last { + padding-right: 0; + border-right: none; } + body ul#resources li.resource div.heading ul.options li { + color: #666666; + font-size: 0.9em; } + body ul#resources li.resource div.heading ul.options li a { + color: #aaaaaa; + text-decoration: none; } + body ul#resources li.resource div.heading ul.options li a:hover { + text-decoration: underline; + color: black; } + body ul#resources li.resource:hover div.heading h2 a, body ul#resources li.resource.active div.heading h2 a { + color: black; } + body ul#resources li.resource:hover div.heading ul.options li a, body ul#resources li.resource.active div.heading ul.options li a { + color: #555555; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #e7f0f7; + border: 1px solid black; + border-color: #c3d9ec; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span { + margin: 0; + padding: 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #0f6ab4; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path { + padding-left: 10px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path a { + color: black; + text-decoration: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path a:hover { + text-decoration: underline; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.first { + padding-left: 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.last { + padding-right: 0; + border-right: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li { + border-right-color: #c3d9ec; + color: #0f6ab4; + font-size: 0.9em; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a { + color: #0f6ab4; + text-decoration: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a.active { + text-decoration: underline; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content { + background-color: #ebf3f9; + border: 1px solid black; + border-color: #c3d9ec; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content h4 { + color: #0f6ab4; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #6fa5d2; + display: inline-block; + font-size: 0.9em; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #e7f6ec; + border: 1px solid black; + border-color: #c3e8d1; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span { + margin: 0; + padding: 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #10a54a; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path { + padding-left: 10px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path a { + color: black; + text-decoration: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path a:hover { + text-decoration: underline; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.first { + padding-left: 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.last { + padding-right: 0; + border-right: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li { + border-right-color: #c3e8d1; + color: #10a54a; + font-size: 0.9em; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a { + color: #10a54a; + text-decoration: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a.active { + text-decoration: underline; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content { + background-color: #ebf7f0; + border: 1px solid black; + border-color: #c3e8d1; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content h4 { + color: #10a54a; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #6fc992; + display: inline-block; + font-size: 0.9em; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #f9f2e9; + border: 1px solid black; + border-color: #f0e0ca; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span { + margin: 0; + padding: 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #c5862b; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path { + padding-left: 10px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path a { + color: black; + text-decoration: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path a:hover { + text-decoration: underline; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.first { + padding-left: 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.last { + padding-right: 0; + border-right: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li { + border-right-color: #f0e0ca; + color: #c5862b; + font-size: 0.9em; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a { + color: #c5862b; + text-decoration: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a.active { + text-decoration: underline; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content { + background-color: #faf5ee; + border: 1px solid black; + border-color: #f0e0ca; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content h4 { + color: #c5862b; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #dcb67f; + display: inline-block; + font-size: 0.9em; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #f5e8e8; + border: 1px solid black; + border-color: #e8c6c7; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span { + margin: 0; + padding: 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #a41e22; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.path { + padding-left: 10px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.path a { + color: black; + text-decoration: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.path a:hover { + text-decoration: underline; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li.first { + padding-left: 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li.last { + padding-right: 0; + border-right: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li { + border-right-color: #e8c6c7; + color: #a41e22; + font-size: 0.9em; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a { + color: #a41e22; + text-decoration: none; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a.active { + text-decoration: underline; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content { + background-color: #f7eded; + border: 1px solid black; + border-color: #e8c6c7; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content h4 { + color: #a41e22; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #c8787a; + display: inline-block; + font-size: 0.9em; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; } + body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; } diff --git a/MediaBrowser.Common.Implementations/swagger-ui/images/pet_store_api.png b/MediaBrowser.Common.Implementations/swagger-ui/images/pet_store_api.png new file mode 100644 index 0000000000..f9f9cd4aeb Binary files /dev/null and b/MediaBrowser.Common.Implementations/swagger-ui/images/pet_store_api.png differ diff --git a/MediaBrowser.Common.Implementations/swagger-ui/images/wordnik_api.png b/MediaBrowser.Common.Implementations/swagger-ui/images/wordnik_api.png new file mode 100644 index 0000000000..dca4f1455a Binary files /dev/null and b/MediaBrowser.Common.Implementations/swagger-ui/images/wordnik_api.png differ diff --git a/MediaBrowser.Common.Implementations/swagger-ui/index.html b/MediaBrowser.Common.Implementations/swagger-ui/index.html new file mode 100644 index 0000000000..5d2a8d88da --- /dev/null +++ b/MediaBrowser.Common.Implementations/swagger-ui/index.html @@ -0,0 +1,88 @@ + + + Swagger UI + + + + + + + + + + + + + + + + + + + + +
+   +
+ +
+ +
+ + + + diff --git a/MediaBrowser.Common.Implementations/swagger-ui/lib/backbone-min.js b/MediaBrowser.Common.Implementations/swagger-ui/lib/backbone-min.js new file mode 100644 index 0000000000..c1c0d4fff2 --- /dev/null +++ b/MediaBrowser.Common.Implementations/swagger-ui/lib/backbone-min.js @@ -0,0 +1,38 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org +(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= +{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= +z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= +{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== +b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: +b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; +a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, +h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); +return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= +{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| +!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); +this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('