aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Session
diff options
context:
space:
mode:
authorJoshua M. Boniface <joshua@boniface.me>2021-08-18 02:46:59 -0400
committerGitHub <noreply@github.com>2021-08-18 02:46:59 -0400
commit72d3f7020ad80ce1a53eeae8c5d57abeb22a4679 (patch)
treedd43e663838cdc7d99a4af565523df58ae23c856 /Emby.Server.Implementations/Session
parent7aef0fce444e6d8e06386553ec7ea1401a01bbb1 (diff)
parente5cbafdb6b47377052e0d638908ef96e30a997d6 (diff)
Merge branch 'master' into patch-2
Diffstat (limited to 'Emby.Server.Implementations/Session')
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs152
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs87
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs9
3 files changed, 137 insertions, 111 deletions
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index dad414142..c4b19f417 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,6 +12,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -58,8 +61,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// The active connections.
/// </summary>
- private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections =
- new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new (StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer;
@@ -129,6 +131,9 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public event EventHandler<SessionEventArgs> SessionActivity;
+ /// <inheritdoc />
+ public event EventHandler<SessionEventArgs> SessionControllerConnected;
+
/// <summary>
/// Gets all connections.
/// </summary>
@@ -196,7 +201,7 @@ namespace Emby.Server.Implementations.Session
{
if (!string.IsNullOrEmpty(info.DeviceId))
{
- var capabilities = GetSavedCapabilities(info.DeviceId);
+ var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
if (capabilities != null)
{
@@ -314,6 +319,19 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
+ public void OnSessionControllerConnected(SessionInfo info)
+ {
+ EventHelper.QueueEventIfNotNull(
+ SessionControllerConnected,
+ this,
+ new SessionEventArgs
+ {
+ SessionInfo = info
+ },
+ _logger);
+ }
+
+ /// <inheritdoc />
public void CloseIfNeeded(SessionInfo session)
{
if (!session.SessionControllers.Any(i => i.IsSessionActive))
@@ -666,7 +684,7 @@ namespace Emby.Server.Implementations.Session
}
}
- var eventArgs = new PlaybackProgressEventArgs
+ var eventArgs = new PlaybackStartEventArgs
{
Item = libraryItem,
Users = users,
@@ -1037,7 +1055,7 @@ namespace Emby.Server.Implementations.Session
var generalCommand = new GeneralCommand
{
- Name = GeneralCommandType.DisplayMessage.ToString()
+ Name = GeneralCommandType.DisplayMessage
};
generalCommand.Arguments["Header"] = command.Header;
@@ -1064,10 +1082,10 @@ namespace Emby.Server.Implementations.Session
AssertCanControl(session, controllingSession);
}
- return SendMessageToSession(session, "GeneralCommand", command, cancellationToken);
+ return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
}
- private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
+ private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, CancellationToken cancellationToken)
{
var controllers = session.SessionControllers;
var messageId = Guid.NewGuid();
@@ -1078,7 +1096,7 @@ namespace Emby.Server.Implementations.Session
}
}
- private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, string name, T data, CancellationToken cancellationToken)
+ private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data, CancellationToken cancellationToken)
{
IEnumerable<Task> GetTasks()
{
@@ -1178,23 +1196,21 @@ namespace Emby.Server.Implementations.Session
}
}
- await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
+ await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
- public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken)
+ public async Task SendSyncPlayCommand(SessionInfo session, SendCommand command, CancellationToken cancellationToken)
{
CheckDisposed();
- var session = GetSessionToRemoteControl(sessionId);
- await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false);
+ await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
- public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken)
+ public async Task SendSyncPlayGroupUpdate<T>(SessionInfo session, GroupUpdate<T> command, CancellationToken cancellationToken)
{
CheckDisposed();
- var session = GetSessionToRemoteControl(sessionId);
- await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false);
+ await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false);
}
private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
@@ -1268,7 +1284,7 @@ namespace Emby.Server.Implementations.Session
{
var generalCommand = new GeneralCommand
{
- Name = GeneralCommandType.DisplayContent.ToString(),
+ Name = GeneralCommandType.DisplayContent,
Arguments =
{
["ItemId"] = command.ItemId,
@@ -1297,7 +1313,7 @@ namespace Emby.Server.Implementations.Session
}
}
- return SendMessageToSession(session, "Playstate", command, cancellationToken);
+ return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
}
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
@@ -1322,7 +1338,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- return SendMessageToSessions(Sessions, "RestartRequired", string.Empty, cancellationToken);
+ return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
}
/// <summary>
@@ -1334,7 +1350,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- return SendMessageToSessions(Sessions, "ServerShuttingDown", string.Empty, cancellationToken);
+ return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
}
/// <summary>
@@ -1348,7 +1364,7 @@ namespace Emby.Server.Implementations.Session
_logger.LogDebug("Beginning SendServerRestartNotification");
- return SendMessageToSessions(Sessions, "ServerRestarting", string.Empty, cancellationToken);
+ return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
}
/// <summary>
@@ -1429,6 +1445,29 @@ namespace Emby.Server.Implementations.Session
return AuthenticateNewSessionInternal(request, false);
}
+ public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token)
+ {
+ var result = _authRepo.Get(new AuthenticationInfoQuery()
+ {
+ AccessToken = token,
+ DeviceId = _appHost.SystemId,
+ Limit = 1
+ });
+
+ if (result.TotalRecordCount == 0)
+ {
+ throw new SecurityException("Unknown quick connect token");
+ }
+
+ var info = result.Items[0];
+ request.UserId = info.UserId;
+
+ // There's no need to keep the quick connect token in the database, as AuthenticateNewSessionInternal() issues a long lived token.
+ _authRepo.Delete(info);
+
+ return AuthenticateNewSessionInternal(request, false);
+ }
+
private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
{
CheckDisposed();
@@ -1439,17 +1478,14 @@ namespace Emby.Server.Implementations.Session
user = _userManager.GetUserById(request.UserId);
}
- if (user == null)
- {
- user = _userManager.GetUserByName(request.Username);
- }
+ user ??= _userManager.GetUserByName(request.Username);
if (enforcePassword)
{
user = await _userManager.AuthenticateUser(
request.Username,
request.Password,
- request.PasswordSha1,
+ null,
request.RemoteEndPoint,
true).ConfigureAwait(false);
}
@@ -1466,6 +1502,14 @@ namespace Emby.Server.Implementations.Session
throw new SecurityException("User is not allowed access from this device.");
}
+ int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
+ int maxActiveSessions = user.MaxActiveSessions;
+ _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions);
+ if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
+ {
+ throw new SecurityException("User is at their maximum number of sessions.");
+ }
+
var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName);
var session = LogSessionActivity(
@@ -1499,23 +1543,26 @@ namespace Emby.Server.Implementations.Session
Limit = 1
}).Items.FirstOrDefault();
- var allExistingForDevice = _authRepo.Get(
- new AuthenticationInfoQuery
- {
- DeviceId = deviceId
- }).Items;
-
- foreach (var auth in allExistingForDevice)
+ if (!string.IsNullOrEmpty(deviceId))
{
- if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
- {
- try
+ var allExistingForDevice = _authRepo.Get(
+ new AuthenticationInfoQuery
{
- Logout(auth);
- }
- catch (Exception ex)
+ DeviceId = deviceId
+ }).Items;
+
+ foreach (var auth in allExistingForDevice)
+ {
+ if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
{
- _logger.LogError(ex, "Error while logging out.");
+ try
+ {
+ Logout(auth);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while logging out.");
+ }
}
}
}
@@ -1651,27 +1698,10 @@ namespace Emby.Server.Implementations.Session
SessionInfo = session
});
- try
- {
- SaveCapabilities(session.DeviceId, capabilities);
- }
- catch (Exception ex)
- {
- _logger.LogError("Error saving device capabilities", ex);
- }
+ _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
}
}
- private ClientCapabilities GetSavedCapabilities(string deviceId)
- {
- return _deviceManager.GetCapabilities(deviceId);
- }
-
- private void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
- {
- _deviceManager.SaveCapabilities(deviceId, capabilities);
- }
-
/// <summary>
/// Converts a BaseItem to a BaseItemInfo.
/// </summary>
@@ -1848,7 +1878,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1861,7 +1891,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken)
+ public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1876,7 +1906,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1885,7 +1915,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 1da7a6473..e9e3ca7f4 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -1,13 +1,15 @@
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Events;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -21,50 +23,48 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// The timeout in seconds after which a WebSocket is considered to be lost.
/// </summary>
- public const int WebSocketLostTimeout = 60;
+ private const int WebSocketLostTimeout = 60;
/// <summary>
/// The keep-alive interval factor; controls how often the watcher will check on the status of the WebSockets.
/// </summary>
- public const float IntervalFactor = 0.2f;
+ private const float IntervalFactor = 0.2f;
/// <summary>
/// The ForceKeepAlive factor; controls when a ForceKeepAlive is sent.
/// </summary>
- public const float ForceKeepAliveFactor = 0.75f;
+ private const float ForceKeepAliveFactor = 0.75f;
/// <summary>
- /// The _session manager.
+ /// Lock used for accesing the KeepAlive cancellation token.
/// </summary>
- private readonly ISessionManager _sessionManager;
+ private readonly object _keepAliveLock = new object();
/// <summary>
- /// The _logger.
+ /// The WebSocket watchlist.
/// </summary>
- private readonly ILogger<SessionWebSocketListener> _logger;
- private readonly ILoggerFactory _loggerFactory;
-
- private readonly IHttpServer _httpServer;
+ private readonly HashSet<IWebSocketConnection> _webSockets = new HashSet<IWebSocketConnection>();
/// <summary>
- /// The KeepAlive cancellation token.
+ /// Lock used for accessing the WebSockets watchlist.
/// </summary>
- private CancellationTokenSource _keepAliveCancellationToken;
+ private readonly object _webSocketsLock = new object();
/// <summary>
- /// Lock used for accesing the KeepAlive cancellation token.
+ /// The _session manager.
/// </summary>
- private readonly object _keepAliveLock = new object();
+ private readonly ISessionManager _sessionManager;
/// <summary>
- /// The WebSocket watchlist.
+ /// The _logger.
/// </summary>
- private readonly HashSet<IWebSocketConnection> _webSockets = new HashSet<IWebSocketConnection>();
+ private readonly ILogger<SessionWebSocketListener> _logger;
+ private readonly ILoggerFactory _loggerFactory;
/// <summary>
- /// Lock used for accesing the WebSockets watchlist.
+ /// The KeepAlive cancellation token.
/// </summary>
- private readonly object _webSocketsLock = new object();
+ private CancellationTokenSource _keepAliveCancellationToken;
/// <summary>
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
@@ -72,32 +72,42 @@ namespace Emby.Server.Implementations.Session
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="loggerFactory">The logger factory.</param>
- /// <param name="httpServer">The HTTP server.</param>
public SessionWebSocketListener(
ILogger<SessionWebSocketListener> logger,
ISessionManager sessionManager,
- ILoggerFactory loggerFactory,
- IHttpServer httpServer)
+ ILoggerFactory loggerFactory)
{
_logger = logger;
_sessionManager = sessionManager;
_loggerFactory = loggerFactory;
- _httpServer = httpServer;
+ }
- httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ StopKeepAlive();
}
- private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
+ /// <summary>
+ /// Processes the message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <returns>Task.</returns>
+ public Task ProcessMessageAsync(WebSocketMessageInfo message)
+ => Task.CompletedTask;
+
+ /// <inheritdoc />
+ public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection)
{
- var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString());
+ var session = GetSession(connection.QueryString, connection.RemoteEndPoint.ToString());
if (session != null)
{
- EnsureController(session, e.Argument);
- await KeepAliveWebSocket(e.Argument).ConfigureAwait(false);
+ EnsureController(session, connection);
+ await KeepAliveWebSocket(connection).ConfigureAwait(false);
}
else
{
- _logger.LogWarning("Unable to determine session based on query string: {0}", e.Argument.QueryString);
+ _logger.LogWarning("Unable to determine session based on query string: {0}", connection.QueryString);
}
}
@@ -118,21 +128,6 @@ namespace Emby.Server.Implementations.Session
return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
}
- /// <inheritdoc />
- public void Dispose()
- {
- _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
- StopKeepAlive();
- }
-
- /// <summary>
- /// Processes the message.
- /// </summary>
- /// <param name="message">The message.</param>
- /// <returns>Task.</returns>
- public Task ProcessMessageAsync(WebSocketMessageInfo message)
- => Task.CompletedTask;
-
private void EnsureController(SessionInfo session, IWebSocketConnection connection)
{
var controllerInfo = session.EnsureController<WebSocketController>(
@@ -140,6 +135,8 @@ namespace Emby.Server.Implementations.Session
var controller = (WebSocketController)controllerInfo.Item1;
controller.AddWebSocket(connection);
+
+ _sessionManager.OnSessionControllerConnected(session);
}
/// <summary>
@@ -316,7 +313,7 @@ namespace Emby.Server.Implementations.Session
return webSocket.SendAsync(
new WebSocketMessage<int>
{
- MessageType = "ForceKeepAlive",
+ MessageType = SessionMessageType.ForceKeepAlive,
Data = WebSocketLostTimeout
},
CancellationToken.None);
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index 94604ca1e..9fa92a53a 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -1,6 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1600
-#nullable enable
using System;
using System.Collections.Generic;
@@ -11,6 +9,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
@@ -54,9 +53,9 @@ namespace Emby.Server.Implementations.Session
connection.Closed += OnConnectionClosed;
}
- private void OnConnectionClosed(object sender, EventArgs e)
+ private void OnConnectionClosed(object? sender, EventArgs e)
{
- var connection = (IWebSocketConnection)sender;
+ var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
_sockets.Remove(connection);
connection.Closed -= OnConnectionClosed;
@@ -65,7 +64,7 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public Task SendMessage<T>(
- string name,
+ SessionMessageType name,
Guid messageId,
T data,
CancellationToken cancellationToken)