aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs162
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs79
-rw-r--r--MediaBrowser.Controller/QuickConnect/IQuickConnect.cs56
-rw-r--r--MediaBrowser.Model/QuickConnect/QuickConnectResult.cs32
-rw-r--r--MediaBrowser.Model/QuickConnect/QuickConnectState.cs23
5 files changed, 94 insertions, 258 deletions
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index 7cfd1fced..6f9797969 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Concurrent;
using System.Globalization;
@@ -9,7 +7,6 @@ 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.Model.QuickConnect;
@@ -22,14 +19,28 @@ 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 name of internal access tokens.
+ /// </summary>
+ private const string TokenName = "QuickConnect";
+
+ /// <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 IServerConfigurationManager _config;
private readonly ILogger<QuickConnectManager> _logger;
- private readonly IAuthenticationRepository _authenticationRepository;
- private readonly IAuthorizationContext _authContext;
private readonly IServerApplicationHost _appHost;
+ private readonly IAuthenticationRepository _authenticationRepository;
/// <summary>
/// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
@@ -38,86 +49,42 @@ namespace Emby.Server.Implementations.QuickConnect
/// <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>
public QuickConnectManager(
IServerConfigurationManager config,
ILogger<QuickConnectManager> logger,
IServerApplicationHost appHost,
- IAuthorizationContext authContext,
IAuthenticationRepository authenticationRepository)
{
_config = config;
_logger = logger;
_appHost = appHost;
- _authContext = authContext;
_authenticationRepository = authenticationRepository;
-
- ReloadConfiguration();
}
- /// <inheritdoc/>
- public int CodeLength { get; set; } = 6;
-
- /// <inheritdoc/>
- public string TokenName { get; set; } = "QuickConnect";
-
- /// <inheritdoc/>
- public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
+ /// <inheritdoc />
+ public bool IsEnabled => _config.Configuration.QuickConnectAvailable;
- /// <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()
- {
- 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();
-
- _logger.LogDebug("Configuration saved");
- }
-
- /// <inheritdoc/>
public QuickConnectResult TryConnect()
{
+ AssertActive();
ExpireRequests();
- if (State != QuickConnectState.Active)
- {
- _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State);
- throw new AuthenticationException("Quick connect is not active on this server");
- }
-
+ 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);
_currentRequests[code] = result;
return result;
@@ -126,12 +93,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");
}
@@ -139,8 +106,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];
@@ -161,10 +131,10 @@ namespace Emby.Server.Implementations.QuickConnect
/// <inheritdoc/>
public 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");
}
@@ -174,16 +144,16 @@ namespace Emby.Server.Implementations.QuickConnect
throw new InvalidOperationException("Request is already authorized");
}
- result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ var token = Guid.NewGuid();
+ result.Authentication = token;
// 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.Now.Add(TimeSpan.FromMinutes(1));
_authenticationRepository.Create(new AuthenticationInfo
{
AppName = TokenName,
- AccessToken = result.Authentication,
+ AccessToken = token.ToString("N", CultureInfo.InvariantCulture),
DateCreated = DateTime.UtcNow,
DeviceId = _appHost.SystemId,
DeviceName = _appHost.FriendlyName,
@@ -196,28 +166,6 @@ namespace Emby.Server.Implementations.QuickConnect
return true;
}
- /// <inheritdoc/>
- public int DeleteAllDevices(Guid user)
- {
- var raw = _authenticationRepository.Get(new AuthenticationInfoQuery()
- {
- DeviceId = _appHost.SystemId,
- UserId = user
- });
-
- var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal));
-
- var removed = 0;
- foreach (var token in tokens)
- {
- _authenticationRepository.Delete(token);
- _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken);
- removed++;
- }
-
- return removed;
- }
-
/// <summary>
/// Dispose.
/// </summary>
@@ -235,7 +183,7 @@ namespace Emby.Server.Implementations.QuickConnect
{
if (disposing)
{
- _rng?.Dispose();
+ _rng.Dispose();
}
}
@@ -247,22 +195,19 @@ 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
foreach (var (_, currentRequest) in _currentRequests)
{
- var added = currentRequest.DateAdded ?? DateTime.UnixEpoch;
- if (expireAll || DateTime.UtcNow > added.AddMinutes(Timeout))
+ if (expireAll || currentRequest.DateAdded > minTime)
{
var code = currentRequest.Code;
_logger.LogDebug("Removing expired request {Code}", code);
@@ -274,10 +219,5 @@ namespace Emby.Server.Implementations.QuickConnect
}
}
}
-
- private void ReloadConfiguration()
- {
- State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable;
- }
}
}
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 4ac849181..3cd1bc6d4 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Model.QuickConnect;
using Microsoft.AspNetCore.Authorization;
@@ -30,13 +31,12 @@ namespace Jellyfin.Api.Controllers
/// Gets the current quick connect state.
/// </summary>
/// <response code="200">Quick connect state returned.</response>
- /// <returns>The current <see cref="QuickConnectState"/>.</returns>
- [HttpGet("Status")]
+ /// <returns>Whether Quick Connect is enabled on the server or not.</returns>
+ [HttpGet("Enabled")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QuickConnectState> GetStatus()
+ public ActionResult<bool> GetEnabled()
{
- _quickConnect.ExpireRequests();
- return _quickConnect.State;
+ return _quickConnect.IsEnabled;
}
/// <summary>
@@ -49,7 +49,14 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QuickConnectResult> Initiate()
{
- return _quickConnect.TryConnect();
+ try
+ {
+ return _quickConnect.TryConnect();
+ }
+ catch (AuthenticationException)
+ {
+ return Unauthorized("Quick connect is disabled");
+ }
}
/// <summary>
@@ -72,42 +79,10 @@ namespace Jellyfin.Api.Controllers
{
return NotFound("Unknown secret");
}
- }
-
- /// <summary>
- /// Temporarily activates quick connect for five minutes.
- /// </summary>
- /// <response code="204">Quick connect has been temporarily activated.</response>
- /// <response code="403">Quick connect is unavailable on this server.</response>
- /// <returns>An <see cref="NoContentResult"/> on success.</returns>
- [HttpPost("Activate")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult Activate()
- {
- if (_quickConnect.State == QuickConnectState.Unavailable)
+ catch (AuthenticationException)
{
- return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable");
+ return Unauthorized("Quick connect is disabled");
}
-
- _quickConnect.Activate();
- return NoContent();
- }
-
- /// <summary>
- /// Enables or disables quick connect.
- /// </summary>
- /// <param name="status">New <see cref="QuickConnectState"/>.</param>
- /// <response code="204">Quick connect state set successfully.</response>
- /// <returns>An <see cref="NoContentResult"/> on success.</returns>
- [HttpPost("Available")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available)
- {
- _quickConnect.SetState(status);
- return NoContent();
}
/// <summary>
@@ -129,26 +104,14 @@ namespace Jellyfin.Api.Controllers
return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
}
- return _quickConnect.AuthorizeRequest(userId.Value, code);
- }
-
- /// <summary>
- /// Deauthorize all quick connect devices for the current user.
- /// </summary>
- /// <response code="200">All quick connect devices were deleted.</response>
- /// <returns>The number of devices that were deleted.</returns>
- [HttpPost("Deauthorize")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<int> Deauthorize()
- {
- var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
- if (!userId.HasValue)
+ try
{
- return 0;
+ return _quickConnect.AuthorizeRequest(userId.Value, code);
+ }
+ catch (AuthenticationException)
+ {
+ return Unauthorized("Quick connect is disabled");
}
-
- return _quickConnect.DeleteAllDevices(userId.Value);
}
}
}
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index c4e709c24..ad34c8604 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using MediaBrowser.Model.QuickConnect;
@@ -11,40 +9,9 @@ namespace MediaBrowser.Controller.QuickConnect
public interface IQuickConnect
{
/// <summary>
- /// Gets or sets the length of user facing codes.
- /// </summary>
- int CodeLength { get; set; }
-
- /// <summary>
- /// Gets or sets the name of internal access tokens.
- /// </summary>
- string TokenName { get; set; }
-
- /// <summary>
- /// Gets the current state of quick connect.
- /// </summary>
- QuickConnectState State { get; }
-
- /// <summary>
- /// Gets or sets the time (in minutes) before quick connect will automatically deactivate.
- /// </summary>
- int Timeout { get; set; }
-
- /// <summary>
- /// Assert that quick connect is currently active and throws an exception if it is not.
- /// </summary>
- void AssertActive();
-
- /// <summary>
- /// Temporarily activates quick connect for a short amount of time.
+ /// Gets a value indicating whether quick connect is enabled or not.
/// </summary>
- void Activate();
-
- /// <summary>
- /// Changes the state of quick connect.
- /// </summary>
- /// <param name="newState">New state to change to.</param>
- void SetState(QuickConnectState newState);
+ bool IsEnabled { get; }
/// <summary>
/// Initiates a new quick connect request.
@@ -66,24 +33,5 @@ namespace MediaBrowser.Controller.QuickConnect
/// <param name="code">Identifying code for the request.</param>
/// <returns>A boolean indicating if the authorization completed successfully.</returns>
bool AuthorizeRequest(Guid userId, string code);
-
- /// <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>
- void ExpireRequests(bool expireAll = false);
-
- /// <summary>
- /// Deletes all quick connect access tokens for the provided user.
- /// </summary>
- /// <param name="user">Guid of the user to delete tokens for.</param>
- /// <returns>A count of the deleted tokens.</returns>
- int DeleteAllDevices(Guid user);
-
- /// <summary>
- /// Generates a short code to display to the user to uniquely identify this request.
- /// </summary>
- /// <returns>A short, unique alphanumeric string.</returns>
- string GenerateCode();
}
}
diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
index 0fa40b6a7..d180d2986 100644
--- a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
+++ b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
@@ -3,38 +3,46 @@ using System;
namespace MediaBrowser.Model.QuickConnect
{
/// <summary>
- /// Stores the result of an incoming quick connect request.
+ /// Stores the state of an quick connect request.
/// </summary>
public class QuickConnectResult
{
/// <summary>
- /// Gets a value indicating whether this request is authorized.
+ /// Initializes a new instance of the <see cref="QuickConnectResult"/> class.
/// </summary>
- public bool Authenticated => !string.IsNullOrEmpty(Authentication);
+ /// <param name="secret">The secret used to query the request state.</param>
+ /// <param name="code">The code used to allow the request.</param>
+ /// <param name="dateAdded">The time when the request was created.</param>
+ public QuickConnectResult(string secret, string code, DateTime dateAdded)
+ {
+ Secret = secret;
+ Code = code;
+ DateAdded = dateAdded;
+ }
/// <summary>
- /// Gets or sets the secret value used to uniquely identify this request. Can be used to retrieve authentication information.
+ /// Gets a value indicating whether this request is authorized.
/// </summary>
- public string? Secret { get; set; }
+ public bool Authenticated => Authentication != null;
/// <summary>
- /// Gets or sets the user facing code used so the user can quickly differentiate this request from others.
+ /// Gets the secret value used to uniquely identify this request. Can be used to retrieve authentication information.
/// </summary>
- public string? Code { get; set; }
+ public string Secret { get; }
/// <summary>
- /// Gets or sets the private access token.
+ /// Gets the user facing code used so the user can quickly differentiate this request from others.
/// </summary>
- public string? Authentication { get; set; }
+ public string Code { get; }
/// <summary>
- /// Gets or sets an error message.
+ /// Gets or sets the private access token.
/// </summary>
- public string? Error { get; set; }
+ public Guid? Authentication { get; set; }
/// <summary>
/// Gets or sets the DateTime that this request was created.
/// </summary>
- public DateTime? DateAdded { get; set; }
+ public DateTime DateAdded { get; set; }
}
}
diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectState.cs b/MediaBrowser.Model/QuickConnect/QuickConnectState.cs
deleted file mode 100644
index f1074f25f..000000000
--- a/MediaBrowser.Model/QuickConnect/QuickConnectState.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace MediaBrowser.Model.QuickConnect
-{
- /// <summary>
- /// Quick connect state.
- /// </summary>
- public enum QuickConnectState
- {
- /// <summary>
- /// This feature has not been opted into and is unavailable until the server administrator chooses to opt-in.
- /// </summary>
- Unavailable = 0,
-
- /// <summary>
- /// The feature is enabled for use on the server but is not currently accepting connection requests.
- /// </summary>
- Available = 1,
-
- /// <summary>
- /// The feature is actively accepting connection requests.
- /// </summary>
- Active = 2
- }
-}