mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-03 19:17:24 -05:00 
			
		
		
		
	Implement Device Cache to replace EFCoreSecondLevelCacheInterceptor Original-merge: b7bc0e1c96553675a490c0bd92a58ad9c5f0d0e1 Merged-by: joshuaboniface <joshua@boniface.me> Backported-by: Bond_009 <bond.009@outlook.com>
		
			
				
	
	
		
			319 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			319 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
#pragma warning disable CS1591
 | 
						|
 | 
						|
using System;
 | 
						|
using System.Collections.Generic;
 | 
						|
using System.Net;
 | 
						|
using System.Threading.Tasks;
 | 
						|
using Jellyfin.Data.Queries;
 | 
						|
using Jellyfin.Extensions;
 | 
						|
using MediaBrowser.Controller;
 | 
						|
using MediaBrowser.Controller.Devices;
 | 
						|
using MediaBrowser.Controller.Library;
 | 
						|
using MediaBrowser.Controller.Net;
 | 
						|
using Microsoft.AspNetCore.Http;
 | 
						|
using Microsoft.EntityFrameworkCore;
 | 
						|
using Microsoft.Net.Http.Headers;
 | 
						|
 | 
						|
namespace Jellyfin.Server.Implementations.Security
 | 
						|
{
 | 
						|
    public class AuthorizationContext : IAuthorizationContext
 | 
						|
    {
 | 
						|
        private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
 | 
						|
        private readonly IUserManager _userManager;
 | 
						|
        private readonly IDeviceManager _deviceManager;
 | 
						|
        private readonly IServerApplicationHost _serverApplicationHost;
 | 
						|
 | 
						|
        public AuthorizationContext(
 | 
						|
            IDbContextFactory<JellyfinDbContext> jellyfinDb,
 | 
						|
            IUserManager userManager,
 | 
						|
            IDeviceManager deviceManager,
 | 
						|
            IServerApplicationHost serverApplicationHost)
 | 
						|
        {
 | 
						|
            _jellyfinDbProvider = jellyfinDb;
 | 
						|
            _userManager = userManager;
 | 
						|
            _deviceManager = deviceManager;
 | 
						|
            _serverApplicationHost = serverApplicationHost;
 | 
						|
        }
 | 
						|
 | 
						|
        public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
 | 
						|
        {
 | 
						|
            if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached is not null)
 | 
						|
            {
 | 
						|
                return Task.FromResult((AuthorizationInfo)cached); // Cache should never contain null
 | 
						|
            }
 | 
						|
 | 
						|
            return GetAuthorization(requestContext);
 | 
						|
        }
 | 
						|
 | 
						|
        public async Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext)
 | 
						|
        {
 | 
						|
            var auth = GetAuthorizationDictionary(requestContext);
 | 
						|
            var authInfo = await GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query).ConfigureAwait(false);
 | 
						|
            return authInfo;
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Gets the authorization.
 | 
						|
        /// </summary>
 | 
						|
        /// <param name="httpContext">The HTTP context.</param>
 | 
						|
        /// <returns>Dictionary{System.StringSystem.String}.</returns>
 | 
						|
        private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext)
 | 
						|
        {
 | 
						|
            var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
 | 
						|
 | 
						|
            httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
 | 
						|
            return authInfo;
 | 
						|
        }
 | 
						|
 | 
						|
        private async Task<AuthorizationInfo> GetAuthorizationInfoFromDictionary(
 | 
						|
            Dictionary<string, string>? auth,
 | 
						|
            IHeaderDictionary headers,
 | 
						|
            IQueryCollection queryString)
 | 
						|
        {
 | 
						|
            string? deviceId = null;
 | 
						|
            string? deviceName = null;
 | 
						|
            string? client = null;
 | 
						|
            string? version = null;
 | 
						|
            string? token = null;
 | 
						|
 | 
						|
            if (auth is not null)
 | 
						|
            {
 | 
						|
                auth.TryGetValue("DeviceId", out deviceId);
 | 
						|
                auth.TryGetValue("Device", out deviceName);
 | 
						|
                auth.TryGetValue("Client", out client);
 | 
						|
                auth.TryGetValue("Version", out version);
 | 
						|
                auth.TryGetValue("Token", out token);
 | 
						|
            }
 | 
						|
 | 
						|
            if (string.IsNullOrEmpty(token))
 | 
						|
            {
 | 
						|
                token = headers["X-Emby-Token"];
 | 
						|
            }
 | 
						|
 | 
						|
            if (string.IsNullOrEmpty(token))
 | 
						|
            {
 | 
						|
                token = headers["X-MediaBrowser-Token"];
 | 
						|
            }
 | 
						|
 | 
						|
            if (string.IsNullOrEmpty(token))
 | 
						|
            {
 | 
						|
                token = queryString["ApiKey"];
 | 
						|
            }
 | 
						|
 | 
						|
            // TODO deprecate this query parameter.
 | 
						|
            if (string.IsNullOrEmpty(token))
 | 
						|
            {
 | 
						|
                token = queryString["api_key"];
 | 
						|
            }
 | 
						|
 | 
						|
            var authInfo = new AuthorizationInfo
 | 
						|
            {
 | 
						|
                Client = client,
 | 
						|
                Device = deviceName,
 | 
						|
                DeviceId = deviceId,
 | 
						|
                Version = version,
 | 
						|
                Token = token,
 | 
						|
                IsAuthenticated = false,
 | 
						|
                HasToken = false
 | 
						|
            };
 | 
						|
 | 
						|
            if (string.IsNullOrWhiteSpace(token))
 | 
						|
            {
 | 
						|
                // Request doesn't contain a token.
 | 
						|
                return authInfo;
 | 
						|
            }
 | 
						|
 | 
						|
            authInfo.HasToken = true;
 | 
						|
            var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
 | 
						|
            await using (dbContext.ConfigureAwait(false))
 | 
						|
            {
 | 
						|
                var device = _deviceManager.GetDevices(
 | 
						|
                    new DeviceQuery
 | 
						|
                    {
 | 
						|
                        AccessToken = token
 | 
						|
                    }).Items.FirstOrDefault();
 | 
						|
 | 
						|
                if (device is not null)
 | 
						|
                {
 | 
						|
                    authInfo.IsAuthenticated = true;
 | 
						|
                    var updateToken = false;
 | 
						|
 | 
						|
                    // TODO: Remove these checks for IsNullOrWhiteSpace
 | 
						|
                    if (string.IsNullOrWhiteSpace(authInfo.Client))
 | 
						|
                    {
 | 
						|
                        authInfo.Client = device.AppName;
 | 
						|
                    }
 | 
						|
 | 
						|
                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
 | 
						|
                    {
 | 
						|
                        authInfo.DeviceId = device.DeviceId;
 | 
						|
                    }
 | 
						|
 | 
						|
                    // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
 | 
						|
                    var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
 | 
						|
 | 
						|
                    if (string.IsNullOrWhiteSpace(authInfo.Device))
 | 
						|
                    {
 | 
						|
                        authInfo.Device = device.DeviceName;
 | 
						|
                    }
 | 
						|
                    else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
 | 
						|
                    {
 | 
						|
                        if (allowTokenInfoUpdate)
 | 
						|
                        {
 | 
						|
                            updateToken = true;
 | 
						|
                            device.DeviceName = authInfo.Device;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
 | 
						|
                    if (string.IsNullOrWhiteSpace(authInfo.Version))
 | 
						|
                    {
 | 
						|
                        authInfo.Version = device.AppVersion;
 | 
						|
                    }
 | 
						|
                    else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
 | 
						|
                    {
 | 
						|
                        if (allowTokenInfoUpdate)
 | 
						|
                        {
 | 
						|
                            updateToken = true;
 | 
						|
                            device.AppVersion = authInfo.Version;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
 | 
						|
                    if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
 | 
						|
                    {
 | 
						|
                        device.DateLastActivity = DateTime.UtcNow;
 | 
						|
                        updateToken = true;
 | 
						|
                    }
 | 
						|
 | 
						|
                    authInfo.User = _userManager.GetUserById(device.UserId);
 | 
						|
 | 
						|
                    if (updateToken)
 | 
						|
                    {
 | 
						|
                        await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                    var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
 | 
						|
                    if (key is not null)
 | 
						|
                    {
 | 
						|
                        authInfo.IsAuthenticated = true;
 | 
						|
                        authInfo.Client = key.Name;
 | 
						|
                        authInfo.Token = key.AccessToken;
 | 
						|
                        if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
 | 
						|
                        {
 | 
						|
                            authInfo.DeviceId = _serverApplicationHost.SystemId;
 | 
						|
                        }
 | 
						|
 | 
						|
                        if (string.IsNullOrWhiteSpace(authInfo.Device))
 | 
						|
                        {
 | 
						|
                            authInfo.Device = _serverApplicationHost.Name;
 | 
						|
                        }
 | 
						|
 | 
						|
                        if (string.IsNullOrWhiteSpace(authInfo.Version))
 | 
						|
                        {
 | 
						|
                            authInfo.Version = _serverApplicationHost.ApplicationVersionString;
 | 
						|
                        }
 | 
						|
 | 
						|
                        authInfo.IsApiKey = true;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
 | 
						|
                return authInfo;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Gets the auth.
 | 
						|
        /// </summary>
 | 
						|
        /// <param name="httpReq">The HTTP request.</param>
 | 
						|
        /// <returns>Dictionary{System.StringSystem.String}.</returns>
 | 
						|
        private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
 | 
						|
        {
 | 
						|
            var auth = httpReq.Headers["X-Emby-Authorization"];
 | 
						|
 | 
						|
            if (string.IsNullOrEmpty(auth))
 | 
						|
            {
 | 
						|
                auth = httpReq.Headers[HeaderNames.Authorization];
 | 
						|
            }
 | 
						|
 | 
						|
            return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Gets the authorization.
 | 
						|
        /// </summary>
 | 
						|
        /// <param name="authorizationHeader">The authorization header.</param>
 | 
						|
        /// <returns>Dictionary{System.StringSystem.String}.</returns>
 | 
						|
        private static Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
 | 
						|
        {
 | 
						|
            var firstSpace = authorizationHeader.IndexOf(' ');
 | 
						|
 | 
						|
            // There should be at least two parts
 | 
						|
            if (firstSpace == -1)
 | 
						|
            {
 | 
						|
                return null;
 | 
						|
            }
 | 
						|
 | 
						|
            var name = authorizationHeader[..firstSpace];
 | 
						|
 | 
						|
            if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
 | 
						|
                && !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
 | 
						|
            {
 | 
						|
                return null;
 | 
						|
            }
 | 
						|
 | 
						|
            // Remove up until the first space
 | 
						|
            authorizationHeader = authorizationHeader[(firstSpace + 1)..];
 | 
						|
            return GetParts(authorizationHeader);
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Get the authorization header components.
 | 
						|
        /// </summary>
 | 
						|
        /// <param name="authorizationHeader">The authorization header.</param>
 | 
						|
        /// <returns>Dictionary{System.StringSystem.String}.</returns>
 | 
						|
        public static Dictionary<string, string> GetParts(ReadOnlySpan<char> authorizationHeader)
 | 
						|
        {
 | 
						|
            var result = new Dictionary<string, string>();
 | 
						|
            var escaped = false;
 | 
						|
            int start = 0;
 | 
						|
            string key = string.Empty;
 | 
						|
 | 
						|
            int i;
 | 
						|
            for (i = 0; i < authorizationHeader.Length; i++)
 | 
						|
            {
 | 
						|
                var token = authorizationHeader[i];
 | 
						|
                if (token == '"' || token == ',')
 | 
						|
                {
 | 
						|
                    // Applying a XOR logic to evaluate whether it is opening or closing a value
 | 
						|
                    escaped = (!escaped) == (token == '"');
 | 
						|
                    if (token == ',' && !escaped)
 | 
						|
                    {
 | 
						|
                        // Meeting a comma after a closing escape char means the value is complete
 | 
						|
                        if (start < i)
 | 
						|
                        {
 | 
						|
                            result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
 | 
						|
                            key = string.Empty;
 | 
						|
                        }
 | 
						|
 | 
						|
                        start = i + 1;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                else if (!escaped && token == '=')
 | 
						|
                {
 | 
						|
                    key = authorizationHeader[start.. i].Trim().ToString();
 | 
						|
                    start = i + 1;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            // Add last value
 | 
						|
            if (start < i)
 | 
						|
            {
 | 
						|
                result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
 | 
						|
            }
 | 
						|
 | 
						|
            return result;
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |