|  | @@ -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;
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  }
 |