< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.QuickConnect.QuickConnectManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
Line coverage
85%
Covered lines: 55
Uncovered lines: 9
Coverable lines: 64
Total lines: 231
Line coverage: 85.9%
Branch coverage
62%
Covered branches: 15
Total branches: 24
Branch coverage: 62.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_IsEnabled()100%11100%
AssertActive()100%22100%
TryConnect(...)100%11100%
CheckRequestStatus(...)100%22100%
GenerateCode()100%22100%
GetAuthorizedRequest(...)50%2.03280%
GenerateSecureRandom(...)100%11100%
ExpireRequests(...)50%75.661638.46%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Globalization;
 4using System.Linq;
 5using System.Security.Cryptography;
 6using System.Threading.Tasks;
 7using MediaBrowser.Common.Extensions;
 8using MediaBrowser.Controller.Authentication;
 9using MediaBrowser.Controller.Configuration;
 10using MediaBrowser.Controller.Net;
 11using MediaBrowser.Controller.QuickConnect;
 12using MediaBrowser.Controller.Session;
 13using MediaBrowser.Model.QuickConnect;
 14using Microsoft.Extensions.Logging;
 15
 16namespace Emby.Server.Implementations.QuickConnect
 17{
 18    /// <summary>
 19    /// Quick connect implementation.
 20    /// </summary>
 21    public class QuickConnectManager : IQuickConnect
 22    {
 23        /// <summary>
 24        /// The length of user facing codes.
 25        /// </summary>
 26        private const int CodeLength = 6;
 27
 28        /// <summary>
 29        /// The time (in minutes) that the quick connect token is valid.
 30        /// </summary>
 31        private const int Timeout = 10;
 32
 3033        private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new();
 3034        private readonly ConcurrentDictionary<string, (DateTime Timestamp, AuthenticationResult AuthenticationResult)> _
 35
 36        private readonly IServerConfigurationManager _config;
 37        private readonly ILogger<QuickConnectManager> _logger;
 38        private readonly ISessionManager _sessionManager;
 39
 40        /// <summary>
 41        /// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
 42        /// Should only be called at server startup when a singleton is created.
 43        /// </summary>
 44        /// <param name="config">Configuration.</param>
 45        /// <param name="logger">Logger.</param>
 46        /// <param name="sessionManager">Session Manager.</param>
 47        public QuickConnectManager(
 48            IServerConfigurationManager config,
 49            ILogger<QuickConnectManager> logger,
 50            ISessionManager sessionManager)
 51        {
 3052            _config = config;
 3053            _logger = logger;
 3054            _sessionManager = sessionManager;
 3055        }
 56
 57        /// <inheritdoc />
 1258        public bool IsEnabled => _config.Configuration.QuickConnectAvailable;
 59
 60        /// <summary>
 61        /// Assert that quick connect is currently active and throws an exception if it is not.
 62        /// </summary>
 63        private void AssertActive()
 64        {
 1065            if (!IsEnabled)
 66            {
 467                throw new AuthenticationException("Quick connect is not active on this server");
 68            }
 669        }
 70
 71        /// <inheritdoc/>
 72        public QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo)
 73        {
 774            ArgumentException.ThrowIfNullOrEmpty(authorizationInfo.DeviceId);
 675            ArgumentException.ThrowIfNullOrEmpty(authorizationInfo.Device);
 576            ArgumentException.ThrowIfNullOrEmpty(authorizationInfo.Client);
 477            ArgumentException.ThrowIfNullOrEmpty(authorizationInfo.Version);
 78
 379            AssertActive();
 280            ExpireRequests();
 81
 282            var secret = GenerateSecureRandom();
 283            var code = GenerateCode();
 284            var result = new QuickConnectResult(
 285                secret,
 286                code,
 287                DateTime.UtcNow,
 288                authorizationInfo.DeviceId,
 289                authorizationInfo.Device,
 290                authorizationInfo.Client,
 291                authorizationInfo.Version);
 92
 293            _currentRequests[code] = result;
 294            return result;
 95        }
 96
 97        /// <inheritdoc/>
 98        public QuickConnectResult CheckRequestStatus(string secret)
 99        {
 3100            AssertActive();
 2101            ExpireRequests();
 102
 2103            string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty
 104
 2105            if (!_currentRequests.TryGetValue(code, out QuickConnectResult? result))
 106            {
 1107                throw new ResourceNotFoundException("Unable to find request with provided secret");
 108            }
 109
 1110            return result;
 111        }
 112
 113        /// <summary>
 114        /// Generates a short code to display to the user to uniquely identify this request.
 115        /// </summary>
 116        /// <returns>A short, unique alphanumeric string.</returns>
 117        private string GenerateCode()
 118        {
 2119            Span<byte> raw = stackalloc byte[4];
 120
 2121            int min = (int)Math.Pow(10, CodeLength - 1);
 2122            int max = (int)Math.Pow(10, CodeLength);
 123
 2124            uint scale = uint.MaxValue;
 4125            while (scale == uint.MaxValue)
 126            {
 2127                RandomNumberGenerator.Fill(raw);
 2128                scale = BitConverter.ToUInt32(raw);
 129            }
 130
 2131            int code = (int)(min + ((max - min) * (scale / (double)uint.MaxValue)));
 2132            return code.ToString(CultureInfo.InvariantCulture);
 133        }
 134
 135        /// <inheritdoc/>
 136        public async Task<bool> AuthorizeRequest(Guid userId, string code)
 137        {
 138            AssertActive();
 139            ExpireRequests();
 140
 141            if (!_currentRequests.TryGetValue(code, out QuickConnectResult? result))
 142            {
 143                throw new ResourceNotFoundException("Unable to find request");
 144            }
 145
 146            if (result.Authenticated)
 147            {
 148                throw new InvalidOperationException("Request is already authorized");
 149            }
 150
 151            // Change the time on the request so it expires one minute into the future. It can't expire immediately as o
 152            result.DateAdded = DateTime.UtcNow.Add(TimeSpan.FromMinutes(1));
 153
 154            var authenticationResult = await _sessionManager.AuthenticateDirect(new AuthenticationRequest
 155            {
 156                UserId = userId,
 157                DeviceId = result.DeviceId,
 158                DeviceName = result.DeviceName,
 159                App = result.AppName,
 160                AppVersion = result.AppVersion
 161            }).ConfigureAwait(false);
 162
 163            _authorizedSecrets[result.Secret] = (DateTime.UtcNow, authenticationResult);
 164            result.Authenticated = true;
 165            _currentRequests[code] = result;
 166
 167            _logger.LogDebug("Authorizing device with code {Code} to login as user {UserId}", code, userId);
 168
 169            return true;
 170        }
 171
 172        /// <inheritdoc/>
 173        public AuthenticationResult GetAuthorizedRequest(string secret)
 174        {
 2175            AssertActive();
 1176            ExpireRequests();
 177
 1178            if (!_authorizedSecrets.TryGetValue(secret, out var result))
 179            {
 1180                throw new ResourceNotFoundException("Unable to find request");
 181            }
 182
 0183            return result.AuthenticationResult;
 184        }
 185
 186        private string GenerateSecureRandom(int length = 32)
 187        {
 2188            Span<byte> bytes = stackalloc byte[length];
 2189            RandomNumberGenerator.Fill(bytes);
 190
 2191            return Convert.ToHexString(bytes);
 192        }
 193
 194        /// <summary>
 195        /// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all req
 196        /// </summary>
 197        /// <param name="expireAll">If true, all requests will be expired.</param>
 198        private void ExpireRequests(bool expireAll = false)
 199        {
 200            // All requests before this timestamp have expired
 6201            var minTime = DateTime.UtcNow.AddMinutes(-Timeout);
 202
 203            // Expire stale connection requests
 16204            foreach (var (_, currentRequest) in _currentRequests)
 205            {
 2206                if (expireAll || currentRequest.DateAdded < minTime)
 207                {
 0208                    var code = currentRequest.Code;
 0209                    _logger.LogDebug("Removing expired request {Code}", code);
 210
 0211                    if (!_currentRequests.TryRemove(code, out _))
 212                    {
 0213                        _logger.LogWarning("Request {Code} already expired", code);
 214                    }
 215                }
 216            }
 217
 12218            foreach (var (secret, (timestamp, _)) in _authorizedSecrets)
 219            {
 0220                if (expireAll || timestamp < minTime)
 221                {
 0222                    _logger.LogDebug("Removing expired secret {Secret}", secret);
 0223                    if (!_authorizedSecrets.TryRemove(secret, out _))
 224                    {
 0225                        _logger.LogWarning("Secret {Secret} already expired", secret);
 226                    }
 227                }
 228            }
 6229        }
 230    }
 231}