From a86b71899ec52c44ddc6c3018e8cc5e9d7ff4d62 Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Thu, 27 Dec 2018 18:27:57 -0500 Subject: Add GPL modules --- .../Net/AuthenticatedAttribute.cs | 69 +++++ MediaBrowser.Controller/Net/AuthorizationInfo.cs | 52 ++++ .../Net/BasePeriodicWebSocketListener.cs | 322 +++++++++++++++++++++ MediaBrowser.Controller/Net/IAuthService.cs | 9 + .../Net/IAuthorizationContext.cs | 21 ++ MediaBrowser.Controller/Net/IHasResultFactory.cs | 17 ++ MediaBrowser.Controller/Net/IHttpResultFactory.cs | 80 +++++ MediaBrowser.Controller/Net/IHttpServer.cs | 39 +++ MediaBrowser.Controller/Net/ISessionContext.cs | 16 + .../Net/IWebSocketConnection.cs | 85 ++++++ MediaBrowser.Controller/Net/IWebSocketListener.cs | 17 ++ MediaBrowser.Controller/Net/SecurityException.cs | 21 ++ MediaBrowser.Controller/Net/StaticResultOptions.cs | 41 +++ .../Net/WebSocketConnectEventArgs.cs | 41 +++ .../Net/WebSocketMessageInfo.cs | 16 + 15 files changed, 846 insertions(+) create mode 100644 MediaBrowser.Controller/Net/AuthenticatedAttribute.cs create mode 100644 MediaBrowser.Controller/Net/AuthorizationInfo.cs create mode 100644 MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs create mode 100644 MediaBrowser.Controller/Net/IAuthService.cs create mode 100644 MediaBrowser.Controller/Net/IAuthorizationContext.cs create mode 100644 MediaBrowser.Controller/Net/IHasResultFactory.cs create mode 100644 MediaBrowser.Controller/Net/IHttpResultFactory.cs create mode 100644 MediaBrowser.Controller/Net/IHttpServer.cs create mode 100644 MediaBrowser.Controller/Net/ISessionContext.cs create mode 100644 MediaBrowser.Controller/Net/IWebSocketConnection.cs create mode 100644 MediaBrowser.Controller/Net/IWebSocketListener.cs create mode 100644 MediaBrowser.Controller/Net/SecurityException.cs create mode 100644 MediaBrowser.Controller/Net/StaticResultOptions.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs create mode 100644 MediaBrowser.Controller/Net/WebSocketMessageInfo.cs (limited to 'MediaBrowser.Controller/Net') diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs new file mode 100644 index 0000000000..2f31b8e665 --- /dev/null +++ b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs @@ -0,0 +1,69 @@ +using System; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes + { + public static IAuthService AuthService { get; set; } + + /// + /// Gets or sets the roles. + /// + /// The roles. + public string Roles { get; set; } + + /// + /// Gets or sets a value indicating whether [escape parental control]. + /// + /// true if [escape parental control]; otherwise, false. + public bool EscapeParentalControl { get; set; } + + /// + /// Gets or sets a value indicating whether [allow before startup wizard]. + /// + /// true if [allow before startup wizard]; otherwise, false. + public bool AllowBeforeStartupWizard { get; set; } + + public bool AllowLocal { get; set; } + + /// + /// The request filter is executed before the service. + /// + /// The http request wrapper + /// The http response wrapper + /// The request DTO + public void RequestFilter(IRequest request, IResponse response, object requestDto) + { + AuthService.Authenticate(request, this); + } + + /// + /// Order in which Request Filters are executed. + /// <0 Executed before global request filters + /// >0 Executed after global request filters + /// + /// The priority. + public int Priority + { + get { return 0; } + } + + public string[] GetRoles() + { + return (Roles ?? string.Empty).Split(new []{ ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public bool AllowLocalOnly { get; set; } + } + + public interface IAuthenticationAttributes + { + bool EscapeParentalControl { get; } + bool AllowBeforeStartupWizard { get; } + bool AllowLocal { get; } + bool AllowLocalOnly { get; } + + string[] GetRoles(); + } +} diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs new file mode 100644 index 0000000000..a68060db5b --- /dev/null +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -0,0 +1,52 @@ +using MediaBrowser.Controller.Entities; +using System; + + +namespace MediaBrowser.Controller.Net +{ + public class AuthorizationInfo + { + /// + /// Gets or sets the user identifier. + /// + /// The user identifier. + public Guid UserId { + get { + if (User == null) { + return Guid.Empty; + } + else { + return User.Id; + } + } + } + + /// + /// Gets or sets the device identifier. + /// + /// The device identifier. + public string DeviceId { get; set; } + /// + /// Gets or sets the device. + /// + /// The device. + public string Device { get; set; } + /// + /// Gets or sets the client. + /// + /// The client. + public string Client { get; set; } + /// + /// Gets or sets the version. + /// + /// The version. + public string Version { get; set; } + /// + /// Gets or sets the token. + /// + /// The token. + public string Token { get; set; } + + public User User { get; set; } + } +} diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs new file mode 100644 index 0000000000..7df96b7776 --- /dev/null +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -0,0 +1,322 @@ +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Threading; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.WebSockets; +using System.Threading.Tasks; +using System.Threading; +using System; + +namespace MediaBrowser.Controller.Net +{ + /// + /// Starts sending data over a web socket periodically when a message is received, and then stops when a corresponding stop message is received + /// + /// The type of the T return data type. + /// The type of the T state type. + public abstract class BasePeriodicWebSocketListener : IWebSocketListener, IDisposable + where TStateType : WebSocketListenerState, new() + where TReturnDataType : class + { + /// + /// The _active connections + /// + protected readonly List> ActiveConnections = + new List>(); + + /// + /// Gets the name. + /// + /// The name. + protected abstract string Name { get; } + + /// + /// Gets the data to send. + /// + /// The state. + /// Task{`1}. + protected abstract Task GetDataToSend(TStateType state, CancellationToken cancellationToken); + + /// + /// The logger + /// + protected ILogger Logger; + + protected ITimerFactory TimerFactory { get; private set; } + + protected BasePeriodicWebSocketListener(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + Logger = logger; + } + + /// + /// Processes the message. + /// + /// The message. + /// Task. + public Task ProcessMessage(WebSocketMessageInfo message) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + if (string.Equals(message.MessageType, Name + "Start", StringComparison.OrdinalIgnoreCase)) + { + Start(message); + } + + if (string.Equals(message.MessageType, Name + "Stop", StringComparison.OrdinalIgnoreCase)) + { + Stop(message); + } + + return Task.FromResult(true); + } + + protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + protected virtual bool SendOnTimer + { + get + { + return false; + } + } + + protected virtual void ParseMessageParams(string[] values) + { + + } + + /// + /// Starts sending messages over a web socket + /// + /// The message. + private void Start(WebSocketMessageInfo message) + { + var vals = message.Data.Split(','); + + var dueTimeMs = long.Parse(vals[0], UsCulture); + var periodMs = long.Parse(vals[1], UsCulture); + + if (vals.Length > 2) + { + ParseMessageParams(vals.Skip(2).ToArray()); + } + + var cancellationTokenSource = new CancellationTokenSource(); + + Logger.Debug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name); + + var timer = SendOnTimer ? + TimerFactory.Create(TimerCallback, message.Connection, Timeout.Infinite, Timeout.Infinite) : + null; + + var state = new TStateType + { + IntervalMs = periodMs, + InitialDelayMs = dueTimeMs + }; + + lock (ActiveConnections) + { + ActiveConnections.Add(new Tuple(message.Connection, cancellationTokenSource, timer, state)); + } + + if (timer != null) + { + timer.Change(TimeSpan.FromMilliseconds(dueTimeMs), TimeSpan.FromMilliseconds(periodMs)); + } + } + + /// + /// Timers the callback. + /// + /// The state. + private void TimerCallback(object state) + { + var connection = (IWebSocketConnection)state; + + Tuple tuple; + + lock (ActiveConnections) + { + tuple = ActiveConnections.FirstOrDefault(c => c.Item1 == connection); + } + + if (tuple == null) + { + return; + } + + if (connection.State != WebSocketState.Open || tuple.Item2.IsCancellationRequested) + { + DisposeConnection(tuple); + return; + } + + SendData(tuple); + } + + protected void SendData(bool force) + { + Tuple[] tuples; + + lock (ActiveConnections) + { + tuples = ActiveConnections + .Where(c => + { + if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested) + { + var state = c.Item4; + + if (force || (DateTime.UtcNow - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs) + { + return true; + } + } + + return false; + }) + .ToArray(); + } + + foreach (var tuple in tuples) + { + SendData(tuple); + } + } + + private async void SendData(Tuple tuple) + { + var connection = tuple.Item1; + + try + { + var state = tuple.Item4; + + var cancellationToken = tuple.Item2.Token; + + var data = await GetDataToSend(state, cancellationToken).ConfigureAwait(false); + + if (data != null) + { + await connection.SendAsync(new WebSocketMessage + { + MessageType = Name, + Data = data + + }, cancellationToken).ConfigureAwait(false); + + state.DateLastSendUtc = DateTime.UtcNow; + } + } + catch (OperationCanceledException) + { + if (tuple.Item2.IsCancellationRequested) + { + DisposeConnection(tuple); + } + } + catch (Exception ex) + { + Logger.ErrorException("Error sending web socket message {0}", ex, Name); + DisposeConnection(tuple); + } + } + + /// + /// Stops sending messages over a web socket + /// + /// The message. + private void Stop(WebSocketMessageInfo message) + { + lock (ActiveConnections) + { + var connection = ActiveConnections.FirstOrDefault(c => c.Item1 == message.Connection); + + if (connection != null) + { + DisposeConnection(connection); + } + } + } + + /// + /// Disposes the connection. + /// + /// The connection. + private void DisposeConnection(Tuple connection) + { + Logger.Debug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); + + var timer = connection.Item3; + + if (timer != null) + { + try + { + timer.Dispose(); + } + catch (ObjectDisposedException) + { + + } + } + + try + { + connection.Item2.Cancel(); + connection.Item2.Dispose(); + } + catch (ObjectDisposedException) + { + + } + + ActiveConnections.Remove(connection); + } + + /// + /// 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) + { + lock (ActiveConnections) + { + foreach (var connection in ActiveConnections.ToArray()) + { + DisposeConnection(connection); + } + } + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + } + } + + public class WebSocketListenerState + { + public DateTime DateLastSendUtc { get; set; } + public long InitialDelayMs { get; set; } + public long IntervalMs { get; set; } + } +} diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs new file mode 100644 index 0000000000..361320250c --- /dev/null +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -0,0 +1,9 @@ +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + public interface IAuthService + { + void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues); + } +} diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs new file mode 100644 index 0000000000..5a9d0aa30c --- /dev/null +++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs @@ -0,0 +1,21 @@ +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + public interface IAuthorizationContext + { + /// + /// Gets the authorization information. + /// + /// The request context. + /// AuthorizationInfo. + AuthorizationInfo GetAuthorizationInfo(object requestContext); + + /// + /// Gets the authorization information. + /// + /// The request context. + /// AuthorizationInfo. + AuthorizationInfo GetAuthorizationInfo(IRequest requestContext); + } +} diff --git a/MediaBrowser.Controller/Net/IHasResultFactory.cs b/MediaBrowser.Controller/Net/IHasResultFactory.cs new file mode 100644 index 0000000000..03144e4b88 --- /dev/null +++ b/MediaBrowser.Controller/Net/IHasResultFactory.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + /// + /// Interface IHasResultFactory + /// Services that require a ResultFactory should implement this + /// + public interface IHasResultFactory : IRequiresRequest + { + /// + /// Gets or sets the result factory. + /// + /// The result factory. + IHttpResultFactory ResultFactory { get; set; } + } +} diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs new file mode 100644 index 0000000000..f8e631de35 --- /dev/null +++ b/MediaBrowser.Controller/Net/IHttpResultFactory.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + /// + /// Interface IHttpResultFactory + /// + public interface IHttpResultFactory + { + /// + /// Gets the result. + /// + /// The content. + /// Type of the content. + /// The response headers. + /// System.Object. + object GetResult(string content, string contentType, IDictionary responseHeaders = null); + + object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary responseHeaders = null); + object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary responseHeaders = null); + object GetResult(IRequest requestContext, string content, string contentType, IDictionary responseHeaders = null); + + object GetRedirectResult(string url); + + object GetResult(IRequest requestContext, T result, IDictionary responseHeaders = null) + where T : class; + + /// + /// Gets the static result. + /// + /// The request context. + /// The cache key. + /// The last date modified. + /// Duration of the cache. + /// Type of the content. + /// The factory fn. + /// The response headers. + /// if set to true [is head request]. + /// System.Object. + Task GetStaticResult(IRequest requestContext, + Guid cacheKey, + DateTime? lastDateModified, + TimeSpan? cacheDuration, + string contentType, Func> factoryFn, + IDictionary responseHeaders = null, + bool isHeadRequest = false); + + /// + /// Gets the static result. + /// + /// The request context. + /// The options. + /// System.Object. + Task GetStaticResult(IRequest requestContext, StaticResultOptions options); + + /// + /// Gets the static file result. + /// + /// The request context. + /// The path. + /// The file share. + /// System.Object. + Task GetStaticFileResult(IRequest requestContext, string path, FileShareMode fileShare = FileShareMode.Read); + + /// + /// Gets the static file result. + /// + /// The request context. + /// The options. + /// System.Object. + Task GetStaticFileResult(IRequest requestContext, + StaticFileResultOptions options); + } +} diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs new file mode 100644 index 0000000000..d2ebadcfa7 --- /dev/null +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Events; + +namespace MediaBrowser.Controller.Net +{ + /// + /// Interface IHttpServer + /// + public interface IHttpServer : IDisposable + { + /// + /// Gets the URL prefix. + /// + /// The URL prefix. + string[] UrlPrefixes { get; } + + /// + /// Stops this instance. + /// + void Stop(); + + /// + /// Occurs when [web socket connected]. + /// + event EventHandler> WebSocketConnected; + + /// + /// Inits this instance. + /// + void Init(IEnumerable services, IEnumerable listener); + + /// + /// If set, all requests will respond with this message + /// + string GlobalResponse { get; set; } + } +} diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs new file mode 100644 index 0000000000..37ddbc2b39 --- /dev/null +++ b/MediaBrowser.Controller/Net/ISessionContext.cs @@ -0,0 +1,16 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Session; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + public interface ISessionContext + { + SessionInfo GetSession(object requestContext); + User GetUser(object requestContext); + + SessionInfo GetSession(IRequest requestContext); + User GetUser(IRequest requestContext); + } +} diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs new file mode 100644 index 0000000000..816e9afcaa --- /dev/null +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -0,0 +1,85 @@ +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; +using System.Net.WebSockets; +using System.Threading.Tasks; +using System.Threading; +using System; + +namespace MediaBrowser.Controller.Net +{ + public interface IWebSocketConnection : IDisposable + { + /// + /// Occurs when [closed]. + /// + event EventHandler Closed; + + /// + /// Gets the id. + /// + /// The id. + Guid Id { get; } + + /// + /// Gets the last activity date. + /// + /// The last activity date. + DateTime LastActivityDate { get; } + + /// + /// Gets or sets the URL. + /// + /// The URL. + string Url { get; set; } + /// + /// Gets or sets the query string. + /// + /// The query string. + QueryParamCollection QueryString { get; set; } + + /// + /// Gets or sets the receive action. + /// + /// The receive action. + Func OnReceive { get; set; } + + /// + /// Gets the state. + /// + /// The state. + WebSocketState State { get; } + + /// + /// Gets the remote end point. + /// + /// The remote end point. + string RemoteEndPoint { get; } + + /// + /// Sends a message asynchronously. + /// + /// + /// The message. + /// The cancellation token. + /// Task. + /// message + Task SendAsync(WebSocketMessage message, CancellationToken cancellationToken); + + /// + /// Sends a message asynchronously. + /// + /// The buffer. + /// The cancellation token. + /// Task. + Task SendAsync(byte[] buffer, CancellationToken cancellationToken); + + /// + /// Sends a message asynchronously. + /// + /// The text. + /// The cancellation token. + /// Task. + /// buffer + Task SendAsync(string text, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Net/IWebSocketListener.cs b/MediaBrowser.Controller/Net/IWebSocketListener.cs new file mode 100644 index 0000000000..29698c1a4c --- /dev/null +++ b/MediaBrowser.Controller/Net/IWebSocketListener.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Net +{ + /// + ///This is an interface for listening to messages coming through a web socket connection + /// + public interface IWebSocketListener + { + /// + /// Processes the message. + /// + /// The message. + /// Task. + Task ProcessMessage(WebSocketMessageInfo message); + } +} diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs new file mode 100644 index 0000000000..b251ab9a92 --- /dev/null +++ b/MediaBrowser.Controller/Net/SecurityException.cs @@ -0,0 +1,21 @@ +using System; + +namespace MediaBrowser.Controller.Net +{ + public class SecurityException : Exception + { + public SecurityException(string message) + : base(message) + { + + } + + public SecurityExceptionType SecurityExceptionType { get; set; } + } + + public enum SecurityExceptionType + { + Unauthenticated = 0, + ParentalControl = 1 + } +} diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs new file mode 100644 index 0000000000..1c9b2586d1 --- /dev/null +++ b/MediaBrowser.Controller/Net/StaticResultOptions.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Net +{ + public class StaticResultOptions + { + public string ContentType { get; set; } + public TimeSpan? CacheDuration { get; set; } + public DateTime? DateLastModified { get; set; } + public Guid CacheKey { get; set; } + + public Func> ContentFactory { get; set; } + + public bool IsHeadRequest { get; set; } + + public IDictionary ResponseHeaders { get; set; } + + public Action OnComplete { get; set; } + public Action OnError { get; set; } + + public string Path { get; set; } + public long? ContentLength { get; set; } + + public FileShareMode FileShare { get; set; } + + public StaticResultOptions() + { + ResponseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + FileShare = FileShareMode.Read; + } + } + + public class StaticFileResultOptions : StaticResultOptions + { + } +} diff --git a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs new file mode 100644 index 0000000000..b200f883a3 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Specialized; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.Net +{ + /// + /// Class WebSocketConnectEventArgs + /// + + public class WebSocketConnectingEventArgs : EventArgs + { + /// + /// Gets or sets the URL. + /// + /// The URL. + public string Url { get; set; } + /// + /// Gets or sets the endpoint. + /// + /// The endpoint. + public string Endpoint { get; set; } + /// + /// Gets or sets the query string. + /// + /// The query string. + public QueryParamCollection QueryString { get; set; } + /// + /// Gets or sets a value indicating whether [allow connection]. + /// + /// true if [allow connection]; otherwise, false. + public bool AllowConnection { get; set; } + + public WebSocketConnectingEventArgs() + { + QueryString = new QueryParamCollection(); + AllowConnection = true; + } + } + +} diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs new file mode 100644 index 0000000000..332f164208 --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs @@ -0,0 +1,16 @@ +using MediaBrowser.Model.Net; + +namespace MediaBrowser.Controller.Net +{ + /// + /// Class WebSocketMessageInfo + /// + public class WebSocketMessageInfo : WebSocketMessage + { + /// + /// Gets or sets the connection. + /// + /// The connection. + public IWebSocketConnection Connection { get; set; } + } +} \ No newline at end of file -- cgit v1.2.3