aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs')
-rw-r--r--Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs231
1 files changed, 107 insertions, 124 deletions
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index 7bed06de3..ae773c658 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -3,14 +3,13 @@ using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
-using MediaBrowser.Common;
+using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
-using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Session;
using MediaBrowser.Model.QuickConnect;
using Microsoft.Extensions.Logging;
@@ -21,14 +20,23 @@ namespace Emby.Server.Implementations.QuickConnect
/// </summary>
public class QuickConnectManager : IQuickConnect, IDisposable
{
- private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
- private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ConcurrentDictionary<string, QuickConnectResult>();
+ /// <summary>
+ /// The length of user facing codes.
+ /// </summary>
+ private const int CodeLength = 6;
+
+ /// <summary>
+ /// The time (in minutes) that the quick connect token is valid.
+ /// </summary>
+ private const int Timeout = 10;
+
+ private readonly RNGCryptoServiceProvider _rng = new ();
+ private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ();
+ private readonly ConcurrentDictionary<string, (DateTime Timestamp, AuthenticationResult AuthenticationResult)> _authorizedSecrets = new ();
private readonly IServerConfigurationManager _config;
private readonly ILogger<QuickConnectManager> _logger;
- private readonly IAuthenticationRepository _authenticationRepository;
- private readonly IAuthorizationContext _authContext;
- private readonly IServerApplicationHost _appHost;
+ private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
@@ -36,87 +44,67 @@ namespace Emby.Server.Implementations.QuickConnect
/// </summary>
/// <param name="config">Configuration.</param>
/// <param name="logger">Logger.</param>
- /// <param name="appHost">Application host.</param>
- /// <param name="authContext">Authentication context.</param>
- /// <param name="authenticationRepository">Authentication repository.</param>
+ /// <param name="sessionManager">Session Manager.</param>
public QuickConnectManager(
IServerConfigurationManager config,
ILogger<QuickConnectManager> logger,
- IServerApplicationHost appHost,
- IAuthorizationContext authContext,
- IAuthenticationRepository authenticationRepository)
+ ISessionManager sessionManager)
{
_config = config;
_logger = logger;
- _appHost = appHost;
- _authContext = authContext;
- _authenticationRepository = authenticationRepository;
-
- ReloadConfiguration();
+ _sessionManager = sessionManager;
}
- /// <inheritdoc/>
- public int CodeLength { get; set; } = 6;
+ /// <inheritdoc />
+ public bool IsEnabled => _config.Configuration.QuickConnectAvailable;
- /// <inheritdoc/>
- public string TokenName { get; set; } = "QuickConnect";
-
- /// <inheritdoc/>
- public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
-
- /// <inheritdoc/>
- public int Timeout { get; set; } = 5;
-
- private DateTime DateActivated { get; set; }
-
- /// <inheritdoc/>
- public void AssertActive()
+ /// <summary>
+ /// Assert that quick connect is currently active and throws an exception if it is not.
+ /// </summary>
+ private void AssertActive()
{
- if (State != QuickConnectState.Active)
+ if (!IsEnabled)
{
- throw new ArgumentException("Quick connect is not active on this server");
+ throw new AuthenticationException("Quick connect is not active on this server");
}
}
/// <inheritdoc/>
- public void Activate()
+ public QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo)
{
- DateActivated = DateTime.UtcNow;
- SetState(QuickConnectState.Active);
- }
-
- /// <inheritdoc/>
- public void SetState(QuickConnectState newState)
- {
- _logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState);
-
- ExpireRequests(true);
-
- State = newState;
- _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active;
- _config.SaveConfiguration();
+ if (string.IsNullOrEmpty(authorizationInfo.DeviceId))
+ {
+ throw new ArgumentException(nameof(authorizationInfo.DeviceId) + " is required");
+ }
- _logger.LogDebug("Configuration saved");
- }
+ if (string.IsNullOrEmpty(authorizationInfo.Device))
+ {
+ throw new ArgumentException(nameof(authorizationInfo.Device) + " is required");
+ }
- /// <inheritdoc/>
- public QuickConnectResult TryConnect()
- {
- ExpireRequests();
+ if (string.IsNullOrEmpty(authorizationInfo.Client))
+ {
+ throw new ArgumentException(nameof(authorizationInfo.Client) + " is required");
+ }
- if (State != QuickConnectState.Active)
+ if (string.IsNullOrEmpty(authorizationInfo.Version))
{
- _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State);
- throw new AuthenticationException("Quick connect is not active on this server");
+ throw new ArgumentException(nameof(authorizationInfo.Version) + "is required");
}
+ AssertActive();
+ ExpireRequests();
+
+ var secret = GenerateSecureRandom();
var code = GenerateCode();
- var result = new QuickConnectResult()
- {
- Secret = GenerateSecureRandom(),
- DateAdded = DateTime.UtcNow,
- Code = code
- };
+ var result = new QuickConnectResult(
+ secret,
+ code,
+ DateTime.UtcNow,
+ authorizationInfo.DeviceId,
+ authorizationInfo.Device,
+ authorizationInfo.Client,
+ authorizationInfo.Version);
_currentRequests[code] = result;
return result;
@@ -125,12 +113,12 @@ namespace Emby.Server.Implementations.QuickConnect
/// <inheritdoc/>
public QuickConnectResult CheckRequestStatus(string secret)
{
- ExpireRequests();
AssertActive();
+ ExpireRequests();
string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First();
- if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
+ if (!_currentRequests.TryGetValue(code, out QuickConnectResult? result))
{
throw new ResourceNotFoundException("Unable to find request with provided secret");
}
@@ -138,8 +126,11 @@ namespace Emby.Server.Implementations.QuickConnect
return result;
}
- /// <inheritdoc/>
- public string GenerateCode()
+ /// <summary>
+ /// Generates a short code to display to the user to uniquely identify this request.
+ /// </summary>
+ /// <returns>A short, unique alphanumeric string.</returns>
+ private string GenerateCode()
{
Span<byte> raw = stackalloc byte[4];
@@ -158,12 +149,12 @@ namespace Emby.Server.Implementations.QuickConnect
}
/// <inheritdoc/>
- public bool AuthorizeRequest(Guid userId, string code)
+ public async Task<bool> AuthorizeRequest(Guid userId, string code)
{
- ExpireRequests();
AssertActive();
+ ExpireRequests();
- if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
+ if (!_currentRequests.TryGetValue(code, out QuickConnectResult? result))
{
throw new ResourceNotFoundException("Unable to find request");
}
@@ -173,48 +164,39 @@ namespace Emby.Server.Implementations.QuickConnect
throw new InvalidOperationException("Request is already authorized");
}
- result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
-
// Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
- var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout));
- result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1));
+ result.DateAdded = DateTime.UtcNow.Add(TimeSpan.FromMinutes(1));
- _authenticationRepository.Create(new AuthenticationInfo
+ var authenticationResult = await _sessionManager.AuthenticateDirect(new AuthenticationRequest
{
- AppName = TokenName,
- AccessToken = result.Authentication,
- DateCreated = DateTime.UtcNow,
- DeviceId = _appHost.SystemId,
- DeviceName = _appHost.FriendlyName,
- AppVersion = _appHost.ApplicationVersionString,
- UserId = userId
- });
+ UserId = userId,
+ DeviceId = result.DeviceId,
+ DeviceName = result.DeviceName,
+ App = result.AppName,
+ AppVersion = result.AppVersion
+ }).ConfigureAwait(false);
+
+ _authorizedSecrets[result.Secret] = (DateTime.UtcNow, authenticationResult);
+ result.Authenticated = true;
+ _currentRequests[code] = result;
- _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
+ _logger.LogDebug("Authorizing device with code {Code} to login as user {UserId}", code, userId);
return true;
}
/// <inheritdoc/>
- public int DeleteAllDevices(Guid user)
+ public AuthenticationResult GetAuthorizedRequest(string secret)
{
- var raw = _authenticationRepository.Get(new AuthenticationInfoQuery()
- {
- DeviceId = _appHost.SystemId,
- UserId = user
- });
-
- var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal));
+ AssertActive();
+ ExpireRequests();
- var removed = 0;
- foreach (var token in tokens)
+ if (!_authorizedSecrets.TryGetValue(secret, out var result))
{
- _authenticationRepository.Delete(token);
- _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken);
- removed++;
+ throw new ResourceNotFoundException("Unable to find request");
}
- return removed;
+ return result.AuthenticationResult;
}
/// <summary>
@@ -234,7 +216,7 @@ namespace Emby.Server.Implementations.QuickConnect
{
if (disposing)
{
- _rng?.Dispose();
+ _rng.Dispose();
}
}
@@ -246,40 +228,41 @@ namespace Emby.Server.Implementations.QuickConnect
return Convert.ToHexString(bytes);
}
- /// <inheritdoc/>
- public void ExpireRequests(bool expireAll = false)
+ /// <summary>
+ /// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all requests are unconditionally expired.
+ /// </summary>
+ /// <param name="expireAll">If true, all requests will be expired.</param>
+ private void ExpireRequests(bool expireAll = false)
{
- // Check if quick connect should be deactivated
- if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll)
- {
- _logger.LogDebug("Quick connect time expired, deactivating");
- SetState(QuickConnectState.Available);
- expireAll = true;
- }
+ // All requests before this timestamp have expired
+ var minTime = DateTime.UtcNow.AddMinutes(-Timeout);
// Expire stale connection requests
- var code = string.Empty;
- var values = _currentRequests.Values.ToList();
-
- for (int i = 0; i < values.Count; i++)
+ foreach (var (_, currentRequest) in _currentRequests)
{
- var added = values[i].DateAdded ?? DateTime.UnixEpoch;
- if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
+ if (expireAll || currentRequest.DateAdded < minTime)
{
- code = values[i].Code;
- _logger.LogDebug("Removing expired request {code}", code);
+ var code = currentRequest.Code;
+ _logger.LogDebug("Removing expired request {Code}", code);
if (!_currentRequests.TryRemove(code, out _))
{
- _logger.LogWarning("Request {code} already expired", code);
+ _logger.LogWarning("Request {Code} already expired", code);
}
}
}
- }
- private void ReloadConfiguration()
- {
- State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable;
+ foreach (var (secret, (timestamp, _)) in _authorizedSecrets)
+ {
+ if (expireAll || timestamp < minTime)
+ {
+ _logger.LogDebug("Removing expired secret {Secret}", secret);
+ if (!_authorizedSecrets.TryRemove(secret, out _))
+ {
+ _logger.LogWarning("Secret {Secret} already expired", secret);
+ }
+ }
+ }
}
}
}