< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Security.AuthorizationContext
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
Line coverage
87%
Covered lines: 35
Uncovered lines: 5
Coverable lines: 40
Total lines: 318
Line coverage: 87.5%
Branch coverage
78%
Covered branches: 25
Total branches: 32
Branch coverage: 78.1%
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%
GetAuthorizationInfo(...)0%2040%
GetAuthorizationDictionary(...)100%44100%
GetAuthorization(...)50%6.4677.77%
GetParts(...)100%1818100%

File(s)

/srv/git/jellyfin/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Net;
 6using System.Threading.Tasks;
 7using Jellyfin.Data.Queries;
 8using Jellyfin.Extensions;
 9using MediaBrowser.Controller;
 10using MediaBrowser.Controller.Devices;
 11using MediaBrowser.Controller.Library;
 12using MediaBrowser.Controller.Net;
 13using Microsoft.AspNetCore.Http;
 14using Microsoft.EntityFrameworkCore;
 15using Microsoft.Net.Http.Headers;
 16
 17namespace Jellyfin.Server.Implementations.Security
 18{
 19    public class AuthorizationContext : IAuthorizationContext
 20    {
 21        private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
 22        private readonly IUserManager _userManager;
 23        private readonly IDeviceManager _deviceManager;
 24        private readonly IServerApplicationHost _serverApplicationHost;
 25
 26        public AuthorizationContext(
 27            IDbContextFactory<JellyfinDbContext> jellyfinDb,
 28            IUserManager userManager,
 29            IDeviceManager deviceManager,
 30            IServerApplicationHost serverApplicationHost)
 31        {
 2132            _jellyfinDbProvider = jellyfinDb;
 2133            _userManager = userManager;
 2134            _deviceManager = deviceManager;
 2135            _serverApplicationHost = serverApplicationHost;
 2136        }
 37
 38        public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
 39        {
 040            if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached is n
 41            {
 042                return Task.FromResult((AuthorizationInfo)cached); // Cache should never contain null
 43            }
 44
 045            return GetAuthorization(requestContext);
 46        }
 47
 48        public async Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext)
 49        {
 50            var auth = GetAuthorizationDictionary(requestContext);
 51            var authInfo = await GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query).
 52            return authInfo;
 53        }
 54
 55        /// <summary>
 56        /// Gets the authorization.
 57        /// </summary>
 58        /// <param name="httpContext">The HTTP context.</param>
 59        /// <returns>Dictionary{System.StringSystem.String}.</returns>
 60        private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext)
 61        {
 62            var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
 63
 64            httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
 65            return authInfo;
 66        }
 67
 68        private async Task<AuthorizationInfo> GetAuthorizationInfoFromDictionary(
 69            Dictionary<string, string>? auth,
 70            IHeaderDictionary headers,
 71            IQueryCollection queryString)
 72        {
 73            string? deviceId = null;
 74            string? deviceName = null;
 75            string? client = null;
 76            string? version = null;
 77            string? token = null;
 78
 79            if (auth is not null)
 80            {
 81                auth.TryGetValue("DeviceId", out deviceId);
 82                auth.TryGetValue("Device", out deviceName);
 83                auth.TryGetValue("Client", out client);
 84                auth.TryGetValue("Version", out version);
 85                auth.TryGetValue("Token", out token);
 86            }
 87
 88            if (string.IsNullOrEmpty(token))
 89            {
 90                token = headers["X-Emby-Token"];
 91            }
 92
 93            if (string.IsNullOrEmpty(token))
 94            {
 95                token = headers["X-MediaBrowser-Token"];
 96            }
 97
 98            if (string.IsNullOrEmpty(token))
 99            {
 100                token = queryString["ApiKey"];
 101            }
 102
 103            // TODO deprecate this query parameter.
 104            if (string.IsNullOrEmpty(token))
 105            {
 106                token = queryString["api_key"];
 107            }
 108
 109            var authInfo = new AuthorizationInfo
 110            {
 111                Client = client,
 112                Device = deviceName,
 113                DeviceId = deviceId,
 114                Version = version,
 115                Token = token,
 116                IsAuthenticated = false,
 117                HasToken = false
 118            };
 119
 120            if (string.IsNullOrWhiteSpace(token))
 121            {
 122                // Request doesn't contain a token.
 123                return authInfo;
 124            }
 125
 126            authInfo.HasToken = true;
 127            var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
 128            await using (dbContext.ConfigureAwait(false))
 129            {
 130                var device = _deviceManager.GetDevices(
 131                    new DeviceQuery
 132                    {
 133                        AccessToken = token
 134                    }).Items.FirstOrDefault();
 135
 136                if (device is not null)
 137                {
 138                    authInfo.IsAuthenticated = true;
 139                    var updateToken = false;
 140
 141                    // TODO: Remove these checks for IsNullOrWhiteSpace
 142                    if (string.IsNullOrWhiteSpace(authInfo.Client))
 143                    {
 144                        authInfo.Client = device.AppName;
 145                    }
 146
 147                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
 148                    {
 149                        authInfo.DeviceId = device.DeviceId;
 150                    }
 151
 152                    // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
 153                    var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCas
 154
 155                    if (string.IsNullOrWhiteSpace(authInfo.Device))
 156                    {
 157                        authInfo.Device = device.DeviceName;
 158                    }
 159                    else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
 160                    {
 161                        if (allowTokenInfoUpdate)
 162                        {
 163                            updateToken = true;
 164                            device.DeviceName = authInfo.Device;
 165                        }
 166                    }
 167
 168                    if (string.IsNullOrWhiteSpace(authInfo.Version))
 169                    {
 170                        authInfo.Version = device.AppVersion;
 171                    }
 172                    else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
 173                    {
 174                        if (allowTokenInfoUpdate)
 175                        {
 176                            updateToken = true;
 177                            device.AppVersion = authInfo.Version;
 178                        }
 179                    }
 180
 181                    if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
 182                    {
 183                        device.DateLastActivity = DateTime.UtcNow;
 184                        updateToken = true;
 185                    }
 186
 187                    authInfo.User = _userManager.GetUserById(device.UserId);
 188
 189                    if (updateToken)
 190                    {
 191                        await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
 192                    }
 193                }
 194                else
 195                {
 196                    var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).Configu
 197                    if (key is not null)
 198                    {
 199                        authInfo.IsAuthenticated = true;
 200                        authInfo.Client = key.Name;
 201                        authInfo.Token = key.AccessToken;
 202                        if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
 203                        {
 204                            authInfo.DeviceId = _serverApplicationHost.SystemId;
 205                        }
 206
 207                        if (string.IsNullOrWhiteSpace(authInfo.Device))
 208                        {
 209                            authInfo.Device = _serverApplicationHost.Name;
 210                        }
 211
 212                        if (string.IsNullOrWhiteSpace(authInfo.Version))
 213                        {
 214                            authInfo.Version = _serverApplicationHost.ApplicationVersionString;
 215                        }
 216
 217                        authInfo.IsApiKey = true;
 218                    }
 219                }
 220
 221                return authInfo;
 222            }
 223        }
 224
 225        /// <summary>
 226        /// Gets the auth.
 227        /// </summary>
 228        /// <param name="httpReq">The HTTP request.</param>
 229        /// <returns>Dictionary{System.StringSystem.String}.</returns>
 230        private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
 231        {
 203232            var auth = httpReq.Headers["X-Emby-Authorization"];
 233
 203234            if (string.IsNullOrEmpty(auth))
 235            {
 203236                auth = httpReq.Headers[HeaderNames.Authorization];
 237            }
 238
 203239            return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
 240        }
 241
 242        /// <summary>
 243        /// Gets the authorization.
 244        /// </summary>
 245        /// <param name="authorizationHeader">The authorization header.</param>
 246        /// <returns>Dictionary{System.StringSystem.String}.</returns>
 247        private static Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
 248        {
 142249            var firstSpace = authorizationHeader.IndexOf(' ');
 250
 251            // There should be at least two parts
 142252            if (firstSpace == -1)
 253            {
 0254                return null;
 255            }
 256
 142257            var name = authorizationHeader[..firstSpace];
 258
 142259            if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
 142260                && !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
 261            {
 0262                return null;
 263            }
 264
 265            // Remove up until the first space
 142266            authorizationHeader = authorizationHeader[(firstSpace + 1)..];
 142267            return GetParts(authorizationHeader);
 268        }
 269
 270        /// <summary>
 271        /// Get the authorization header components.
 272        /// </summary>
 273        /// <param name="authorizationHeader">The authorization header.</param>
 274        /// <returns>Dictionary{System.StringSystem.String}.</returns>
 275        public static Dictionary<string, string> GetParts(ReadOnlySpan<char> authorizationHeader)
 276        {
 147277            var result = new Dictionary<string, string>();
 147278            var escaped = false;
 147279            int start = 0;
 147280            string key = string.Empty;
 281
 282            int i;
 38538283            for (i = 0; i < authorizationHeader.Length; i++)
 284            {
 19122285                var token = authorizationHeader[i];
 19122286                if (token == '"' || token == ',')
 287                {
 288                    // Applying a XOR logic to evaluate whether it is opening or closing a value
 1689289                    escaped = (!escaped) == (token == '"');
 1689290                    if (token == ',' && !escaped)
 291                    {
 292                        // Meeting a comma after a closing escape char means the value is complete
 539293                        if (start < i)
 294                        {
 539295                            result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
 539296                            key = string.Empty;
 297                        }
 298
 539299                        start = i + 1;
 300                    }
 301                }
 17433302                else if (!escaped && token == '=')
 303                {
 686304                    key = authorizationHeader[start.. i].Trim().ToString();
 686305                    start = i + 1;
 306                }
 307            }
 308
 309            // Add last value
 147310            if (start < i)
 311            {
 147312                result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
 313            }
 314
 147315            return result;
 316        }
 317    }
 318}