From 4f67fc4aefc11c1a4293227c70de922dbe03c652 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Thu, 7 Mar 2013 00:34:00 -0500 Subject: removed base kernel and ikernel --- .../HttpServer/BaseRestService.cs | 458 +++++++ .../HttpServer/HttpServer.cs | 570 +++++++++ .../HttpServer/NativeWebSocket.cs | 165 +++ .../HttpServer/ServerFactory.cs | 27 + .../HttpServer/StreamWriter.cs | 48 + .../HttpServer/SwaggerService.cs | 38 + .../MediaBrowser.Server.Implementations.csproj | 101 ++ MediaBrowser.Server.Implementations/README.txt | 41 + .../ServerManager/RegisterServer.bat | 28 + .../ServerManager/ServerManager.cs | 525 ++++++++ .../ServerManager/WebSocketConnection.cs | 225 ++++ .../Sqlite/SQLiteDisplayPreferencesRepository.cs | 1 - .../Sqlite/SQLiteItemRepository.cs | 1 - .../Sqlite/SQLiteUserDataRepository.cs | 1 - .../Sqlite/SQLiteUserRepository.cs | 1 - .../Udp/UdpServer.cs | 203 +++ .../Updates/InstallationManager.cs | 14 +- .../WebSocket/AlchemyServer.cs | 113 ++ .../WebSocket/AlchemyWebSocket.cs | 132 ++ .../packages.config | 12 + .../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, 5878 insertions(+), 14 deletions(-) create mode 100644 MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/HttpServer.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs create mode 100644 MediaBrowser.Server.Implementations/README.txt create mode 100644 MediaBrowser.Server.Implementations/ServerManager/RegisterServer.bat create mode 100644 MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs create mode 100644 MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs create mode 100644 MediaBrowser.Server.Implementations/Udp/UdpServer.cs create mode 100644 MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs create mode 100644 MediaBrowser.Server.Implementations/WebSocket/AlchemyWebSocket.cs create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/css/screen.css create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/images/pet_store_api.png create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/images/wordnik_api.png create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/index.html create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/lib/backbone-min.js create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/lib/handlebars.runtime-1.0.0.beta.6.js create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/lib/jquery.ba-bbq.min.js create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/lib/jquery.min.js create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/lib/jquery.slideto.min.js create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/lib/jquery.wiggle.min.js create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/lib/swagger.js create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/lib/underscore-min.js create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/swagger-ui.js create mode 100644 MediaBrowser.Server.Implementations/swagger-ui/swagger-ui.min.js (limited to 'MediaBrowser.Server.Implementations') diff --git a/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs b/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs new file mode 100644 index 0000000000..b847084379 --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs @@ -0,0 +1,458 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; +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.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MimeTypes = MediaBrowser.Common.Net.MimeTypes; + +namespace MediaBrowser.Server.Implementations.HttpServer +{ + /// + /// Class BaseRestService + /// + public class BaseRestService : Service, IRestfulService + { + /// + /// 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 null so that service stack won't do anything + return null; + } + + 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 null so that service stack won't do anything + return null; + } + + 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 null so that service stack won't do anything + return null; + } + + 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; + } + + Response.StatusCode = 304; + + return new byte[]{}; + } + + 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)); + } + } + + /// + /// Gets the routes. + /// + /// IEnumerable{RouteInfo}. + public IEnumerable GetRoutes() + { + return new RouteInfo[] {}; + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpServer.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpServer.cs new file mode 100644 index 0000000000..ee5009e21a --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpServer.cs @@ -0,0 +1,570 @@ +using Funq; +using MediaBrowser.Common; +using MediaBrowser.Common.Extensions; +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.Server.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 url = context.Request.Url.ToString(); + var endPoint = context.Request.RemoteEndPoint; + + 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, url, endPoint); + httpRes.Close(); + return; + } + + throw new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo); + } + + /// + /// Logs the response. + /// + /// The CTX. + private void LogResponse(HttpListenerContext ctx, string url, IPEndPoint endPoint) + { + if (!EnableHttpRequestLogging) + { + return; + } + + var statusode = ctx.Response.StatusCode; + + var log = new StringBuilder(); + + log.AppendLine(string.Format("Url: {0}", url)); + + log.AppendLine("Headers: " + string.Join(",", ctx.Response.Headers.AllKeys.Select(k => k + "=" + ctx.Response.Headers[k]))); + + var msg = "Http Response Sent (" + statusode + ") to " + endPoint; + + _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)); + + foreach (var route in services.SelectMany(i => i.GetRoutes())) + { + Routes.Add(route.RequestType, route.Path, route.Verbs); + } + + Init(); + } + } + + /// + /// Class ContainerAdapter + /// + class ContainerAdapter : IContainerAdapter, IRelease + { + /// + /// 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(); + } + + /// + /// Releases the specified instance. + /// + /// The instance. + public void Release(object instance) + { + // Leave this empty so SS doesn't try to dispose our objects + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs b/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs new file mode 100644 index 0000000000..9f5d14eaf9 --- /dev/null +++ b/MediaBrowser.Server.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.Server.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.Server.Implementations/HttpServer/ServerFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs new file mode 100644 index 0000000000..75e077c38f --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs @@ -0,0 +1,27 @@ +using MediaBrowser.Common; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Server.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.Server.Implementations/HttpServer/StreamWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs new file mode 100644 index 0000000000..c4524280dd --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs @@ -0,0 +1,48 @@ +using ServiceStack.Service; +using System.IO; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.HttpServer +{ + /// + /// Class StreamWriter + /// + public class StreamWriter : IStreamWriter + { + /// + /// Gets or sets the source stream. + /// + /// The source stream. + public Stream SourceStream { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The source. + public StreamWriter(Stream source) + { + SourceStream = source; + } + + /// + /// Writes to. + /// + /// The response stream. + public void WriteTo(Stream responseStream) + { + var task = WriteToAsync(responseStream); + + Task.WaitAll(task); + } + + /// + /// Writes to async. + /// + /// The response stream. + /// Task. + private Task WriteToAsync(Stream responseStream) + { + return SourceStream.CopyToAsync(responseStream); + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs b/MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs new file mode 100644 index 0000000000..18ab40d935 --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs @@ -0,0 +1,38 @@ +using ServiceStack.ServiceHost; +using System.Diagnostics; +using System.IO; + +namespace MediaBrowser.Server.Implementations.HttpServer +{ + /// + /// Class GetDashboardResource + /// + [Route("/swagger-ui/{ResourceName*}", "GET")] + public class GetSwaggerResource + { + /// + /// Gets or sets the name. + /// + /// The name. + public string ResourceName { get; set; } + } + + public class SwaggerService : BaseRestService + { + /// + /// Gets the specified request. + /// + /// The request. + /// System.Object. + public object Get(GetSwaggerResource request) + { + var runningDirectory = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); + + var swaggerDirectory = Path.Combine(runningDirectory, "swagger-ui"); + + var requestedFile = Path.Combine(swaggerDirectory, request.ResourceName.Replace('/', '\\')); + + return ToStaticFileResult(requestedFile); + } + } +} diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 45aa3e5309..e56c79dedd 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -32,6 +32,9 @@ 4 + + ..\packages\Alchemy.2.2.1\lib\net40\Alchemy.dll + False ..\packages\MediaBrowser.BdInfo.1.0.0.2\lib\net45\BdInfo.dll @@ -39,6 +42,39 @@ ..\packages\morelinq.1.0.15631-beta\lib\net35\MoreLinq.dll + + ..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll + + + ..\packages\ServiceStack.3.9.38\lib\net35\ServiceStack.dll + + + ..\packages\ServiceStack.Api.Swagger.3.9.38\lib\net35\ServiceStack.Api.Swagger.dll + + + ..\packages\ServiceStack.Common.3.9.38\lib\net35\ServiceStack.Common.dll + + + ..\packages\ServiceStack.Common.3.9.38\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.39\lib\ServiceStack.OrmLite.dll + + + ..\packages\ServiceStack.OrmLite.SqlServer.3.9.39\lib\ServiceStack.OrmLite.SqlServer.dll + + + ..\packages\ServiceStack.Redis.3.9.38\lib\net35\ServiceStack.Redis.dll + + + ..\packages\ServiceStack.3.9.38\lib\net35\ServiceStack.ServiceInterface.dll + + + ..\packages\ServiceStack.Text.3.9.38\lib\net35\ServiceStack.Text.dll + @@ -47,6 +83,16 @@ ..\packages\System.Data.SQLite.1.0.84.0\lib\net45\System.Data.SQLite.Linq.dll + + ..\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,6 +105,12 @@ + + + + + + @@ -82,13 +134,18 @@ + + + + + @@ -110,6 +167,49 @@ + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + Always @@ -119,6 +219,7 @@ + diff --git a/MediaBrowser.Server.Implementations/README.txt b/MediaBrowser.Server.Implementations/README.txt new file mode 100644 index 0000000000..e8ce34205d --- /dev/null +++ b/MediaBrowser.Server.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.Server.Implementations/ServerManager/RegisterServer.bat b/MediaBrowser.Server.Implementations/ServerManager/RegisterServer.bat new file mode 100644 index 0000000000..d762dfaf76 --- /dev/null +++ b/MediaBrowser.Server.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.Server.Implementations/ServerManager/ServerManager.cs b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs new file mode 100644 index 0000000000..e4cb83e96d --- /dev/null +++ b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs @@ -0,0 +1,525 @@ +using MediaBrowser.Common; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +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.Server.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; + + /// + /// Gets or sets the configuration manager. + /// + /// The configuration manager. + private IServerConfigurationManager ConfigurationManager { get; set; } + + /// + /// 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 ? ConfigurationManager.Configuration.HttpServerPortNumber : ConfigurationManager.Configuration.LegacyWebSocketPortNumber; } + } + + /// + /// Gets the web socket listeners. + /// + /// The web socket listeners. + private readonly List _webSocketListeners = new List(); + + private Kernel _kernel; + + /// + /// Initializes a new instance of the class. + /// + /// The application host. + /// The network manager. + /// The json serializer. + /// The logger. + /// The configuration manager. + /// applicationHost + public ServerManager(IApplicationHost applicationHost, INetworkManager networkManager, IJsonSerializer jsonSerializer, ILogger logger, IServerConfigurationManager configurationManager, Kernel kernel) + { + if (applicationHost == null) + { + throw new ArgumentNullException("applicationHost"); + } + 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; + _applicationHost = applicationHost; + _networkManager = networkManager; + ConfigurationManager = configurationManager; + _kernel = kernel; + } + + /// + /// Starts this instance. + /// + public void Start() + { + if (_applicationHost.IsFirstRun) + { + RegisterServerWithAdministratorAccess(); + } + + ReloadUdpServer(); + ReloadHttpServer(); + + if (!SupportsNativeWebSocket) + { + ReloadExternalWebSocketServer(); + } + + ConfigurationManager.ConfigurationUpdated += _kernel_ConfigurationUpdated; + } + + /// + /// Starts the external web socket server. + /// + private void ReloadExternalWebSocketServer() + { + DisposeExternalWebSocketServer(); + + ExternalWebSocketServer = _applicationHost.Resolve(); + + ExternalWebSocketServer.Start(ConfigurationManager.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 = ConfigurationManager.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 = _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; + } + + 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 context = "Server"; + + var expectedMessage = String.Format("who is MediaBrowser{0}?", context); + 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}", context, _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) + { + 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(ConfigurationManager.CommonApplicationPaths.TempDirectory, Guid.NewGuid() + ".bat"); + + // Extract the bat file + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MediaBrowser.Server.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}", ConfigurationManager.Configuration.HttpServerPortNumber, + _kernel.HttpServerUrlPrefix, + _kernel.UdpServerPortNumber, + ConfigurationManager.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 = ConfigurationManager.Configuration.EnableHttpLevelLogging; + + if (!string.Equals(HttpServer.UrlPrefix, _kernel.HttpServerUrlPrefix, StringComparison.OrdinalIgnoreCase)) + { + ReloadHttpServer(); + } + + if (!SupportsNativeWebSocket && ExternalWebSocketServer != null && ExternalWebSocketServer.Port != ConfigurationManager.Configuration.LegacyWebSocketPortNumber) + { + ReloadExternalWebSocketServer(); + } + } + + /// + /// Adds the web socket listeners. + /// + /// The listeners. + public void AddWebSocketListeners(IEnumerable listeners) + { + _webSocketListeners.AddRange(listeners); + } + } +} diff --git a/MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs b/MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs new file mode 100644 index 0000000000..d92c607732 --- /dev/null +++ b/MediaBrowser.Server.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.Server.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.Server.Implementations/Sqlite/SQLiteDisplayPreferencesRepository.cs b/MediaBrowser.Server.Implementations/Sqlite/SQLiteDisplayPreferencesRepository.cs index d6a0858afd..ddf44d4071 100644 --- a/MediaBrowser.Server.Implementations/Sqlite/SQLiteDisplayPreferencesRepository.cs +++ b/MediaBrowser.Server.Implementations/Sqlite/SQLiteDisplayPreferencesRepository.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Kernel; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Server.Implementations/Sqlite/SQLiteItemRepository.cs b/MediaBrowser.Server.Implementations/Sqlite/SQLiteItemRepository.cs index 87cce1785d..6a9a9f6f5d 100644 --- a/MediaBrowser.Server.Implementations/Sqlite/SQLiteItemRepository.cs +++ b/MediaBrowser.Server.Implementations/Sqlite/SQLiteItemRepository.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Kernel; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Logging; diff --git a/MediaBrowser.Server.Implementations/Sqlite/SQLiteUserDataRepository.cs b/MediaBrowser.Server.Implementations/Sqlite/SQLiteUserDataRepository.cs index 64ba4d1e2c..dafdce798f 100644 --- a/MediaBrowser.Server.Implementations/Sqlite/SQLiteUserDataRepository.cs +++ b/MediaBrowser.Server.Implementations/Sqlite/SQLiteUserDataRepository.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Kernel; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Logging; diff --git a/MediaBrowser.Server.Implementations/Sqlite/SQLiteUserRepository.cs b/MediaBrowser.Server.Implementations/Sqlite/SQLiteUserRepository.cs index 89d2900dd2..43dffc5964 100644 --- a/MediaBrowser.Server.Implementations/Sqlite/SQLiteUserRepository.cs +++ b/MediaBrowser.Server.Implementations/Sqlite/SQLiteUserRepository.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Kernel; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Logging; diff --git a/MediaBrowser.Server.Implementations/Udp/UdpServer.cs b/MediaBrowser.Server.Implementations/Udp/UdpServer.cs new file mode 100644 index 0000000000..c6869c12d7 --- /dev/null +++ b/MediaBrowser.Server.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.Server.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.Server.Implementations/Updates/InstallationManager.cs b/MediaBrowser.Server.Implementations/Updates/InstallationManager.cs index e17ed03016..bdbf3896ff 100644 --- a/MediaBrowser.Server.Implementations/Updates/InstallationManager.cs +++ b/MediaBrowser.Server.Implementations/Updates/InstallationManager.cs @@ -1,11 +1,9 @@ using MediaBrowser.Common; using MediaBrowser.Common.Events; -using MediaBrowser.Common.Kernel; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Progress; using MediaBrowser.Common.Updates; -using MediaBrowser.Controller; using MediaBrowser.Controller.Updates; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; @@ -72,7 +70,7 @@ namespace MediaBrowser.Server.Implementations.Updates EventHelper.QueueEventIfNotNull(PluginUpdated, this, new GenericEventArgs> { Argument = new Tuple(plugin, newVersion) }, _logger); - Kernel.NotifyPendingRestart(); + ApplicationHost.NotifyPendingRestart(); } #endregion @@ -91,7 +89,7 @@ namespace MediaBrowser.Server.Implementations.Updates EventHelper.QueueEventIfNotNull(PluginInstalled, this, new GenericEventArgs { Argument = package }, _logger); - Kernel.NotifyPendingRestart(); + ApplicationHost.NotifyPendingRestart(); } #endregion @@ -123,19 +121,16 @@ namespace MediaBrowser.Server.Implementations.Updates /// The application host. protected IApplicationHost ApplicationHost { get; private set; } - private IKernel Kernel { get; set; } - /// /// Initializes a new instance of the class. /// - /// The kernel. /// The HTTP client. /// The package manager. /// The json serializer. /// The logger. /// The app host. /// zipClient - public InstallationManager(IKernel kernel, IHttpClient httpClient, IPackageManager packageManager, IJsonSerializer jsonSerializer, ILogger logger, IApplicationHost appHost) + public InstallationManager(IHttpClient httpClient, IPackageManager packageManager, IJsonSerializer jsonSerializer, ILogger logger, IApplicationHost appHost) { if (packageManager == null) { @@ -161,7 +156,6 @@ namespace MediaBrowser.Server.Implementations.Updates ApplicationHost = appHost; _packageManager = packageManager; _logger = logger; - Kernel = kernel; } /// @@ -454,7 +448,7 @@ namespace MediaBrowser.Server.Implementations.Updates OnPluginUninstalled(plugin); - Kernel.NotifyPendingRestart(); + ApplicationHost.NotifyPendingRestart(); } /// diff --git a/MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs b/MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs new file mode 100644 index 0000000000..37490c3d5b --- /dev/null +++ b/MediaBrowser.Server.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.Server.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.Server.Implementations/WebSocket/AlchemyWebSocket.cs b/MediaBrowser.Server.Implementations/WebSocket/AlchemyWebSocket.cs new file mode 100644 index 0000000000..13a9ce0505 --- /dev/null +++ b/MediaBrowser.Server.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.Server.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.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index fb84d9aa7c..002d5974e3 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -1,6 +1,18 @@  + + + + + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/swagger-ui/css/screen.css b/MediaBrowser.Server.Implementations/swagger-ui/css/screen.css new file mode 100644 index 0000000000..07773f9efe --- /dev/null +++ b/MediaBrowser.Server.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.Server.Implementations/swagger-ui/images/pet_store_api.png b/MediaBrowser.Server.Implementations/swagger-ui/images/pet_store_api.png new file mode 100644 index 0000000000..f9f9cd4aeb Binary files /dev/null and b/MediaBrowser.Server.Implementations/swagger-ui/images/pet_store_api.png differ diff --git a/MediaBrowser.Server.Implementations/swagger-ui/images/wordnik_api.png b/MediaBrowser.Server.Implementations/swagger-ui/images/wordnik_api.png new file mode 100644 index 0000000000..dca4f1455a Binary files /dev/null and b/MediaBrowser.Server.Implementations/swagger-ui/images/wordnik_api.png differ diff --git a/MediaBrowser.Server.Implementations/swagger-ui/index.html b/MediaBrowser.Server.Implementations/swagger-ui/index.html new file mode 100644 index 0000000000..0b2b02b10e --- /dev/null +++ b/MediaBrowser.Server.Implementations/swagger-ui/index.html @@ -0,0 +1,88 @@ + + + Swagger UI + + + + + + + + + + + + + + + + + + + + +
+   +
+ +
+ +
+ + + + diff --git a/MediaBrowser.Server.Implementations/swagger-ui/lib/backbone-min.js b/MediaBrowser.Server.Implementations/swagger-ui/lib/backbone-min.js new file mode 100644 index 0000000000..c1c0d4fff2 --- /dev/null +++ b/MediaBrowser.Server.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('