| | 1 | | #pragma warning disable CS1591 |
| | 2 | |
|
| | 3 | | using System; |
| | 4 | | using System.Collections.Generic; |
| | 5 | | using System.Net; |
| | 6 | | using System.Threading.Tasks; |
| | 7 | | using Jellyfin.Data.Queries; |
| | 8 | | using Jellyfin.Extensions; |
| | 9 | | using MediaBrowser.Controller; |
| | 10 | | using MediaBrowser.Controller.Devices; |
| | 11 | | using MediaBrowser.Controller.Library; |
| | 12 | | using MediaBrowser.Controller.Net; |
| | 13 | | using Microsoft.AspNetCore.Http; |
| | 14 | | using Microsoft.EntityFrameworkCore; |
| | 15 | | using Microsoft.Net.Http.Headers; |
| | 16 | |
|
| | 17 | | namespace 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 | | { |
| 21 | 32 | | _jellyfinDbProvider = jellyfinDb; |
| 21 | 33 | | _userManager = userManager; |
| 21 | 34 | | _deviceManager = deviceManager; |
| 21 | 35 | | _serverApplicationHost = serverApplicationHost; |
| 21 | 36 | | } |
| | 37 | |
|
| | 38 | | public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext) |
| | 39 | | { |
| 0 | 40 | | if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached is n |
| | 41 | | { |
| 0 | 42 | | return Task.FromResult((AuthorizationInfo)cached); // Cache should never contain null |
| | 43 | | } |
| | 44 | |
|
| 0 | 45 | | 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 | | { |
| 203 | 232 | | var auth = httpReq.Headers["X-Emby-Authorization"]; |
| | 233 | |
|
| 203 | 234 | | if (string.IsNullOrEmpty(auth)) |
| | 235 | | { |
| 203 | 236 | | auth = httpReq.Headers[HeaderNames.Authorization]; |
| | 237 | | } |
| | 238 | |
|
| 203 | 239 | | 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 | | { |
| 142 | 249 | | var firstSpace = authorizationHeader.IndexOf(' '); |
| | 250 | |
|
| | 251 | | // There should be at least two parts |
| 142 | 252 | | if (firstSpace == -1) |
| | 253 | | { |
| 0 | 254 | | return null; |
| | 255 | | } |
| | 256 | |
|
| 142 | 257 | | var name = authorizationHeader[..firstSpace]; |
| | 258 | |
|
| 142 | 259 | | if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase) |
| 142 | 260 | | && !name.Equals("Emby", StringComparison.OrdinalIgnoreCase)) |
| | 261 | | { |
| 0 | 262 | | return null; |
| | 263 | | } |
| | 264 | |
|
| | 265 | | // Remove up until the first space |
| 142 | 266 | | authorizationHeader = authorizationHeader[(firstSpace + 1)..]; |
| 142 | 267 | | 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 | | { |
| 147 | 277 | | var result = new Dictionary<string, string>(); |
| 147 | 278 | | var escaped = false; |
| 147 | 279 | | int start = 0; |
| 147 | 280 | | string key = string.Empty; |
| | 281 | |
|
| | 282 | | int i; |
| 38538 | 283 | | for (i = 0; i < authorizationHeader.Length; i++) |
| | 284 | | { |
| 19122 | 285 | | var token = authorizationHeader[i]; |
| 19122 | 286 | | if (token == '"' || token == ',') |
| | 287 | | { |
| | 288 | | // Applying a XOR logic to evaluate whether it is opening or closing a value |
| 1689 | 289 | | escaped = (!escaped) == (token == '"'); |
| 1689 | 290 | | if (token == ',' && !escaped) |
| | 291 | | { |
| | 292 | | // Meeting a comma after a closing escape char means the value is complete |
| 539 | 293 | | if (start < i) |
| | 294 | | { |
| 539 | 295 | | result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString()); |
| 539 | 296 | | key = string.Empty; |
| | 297 | | } |
| | 298 | |
|
| 539 | 299 | | start = i + 1; |
| | 300 | | } |
| | 301 | | } |
| 17433 | 302 | | else if (!escaped && token == '=') |
| | 303 | | { |
| 686 | 304 | | key = authorizationHeader[start.. i].Trim().ToString(); |
| 686 | 305 | | start = i + 1; |
| | 306 | | } |
| | 307 | | } |
| | 308 | |
|
| | 309 | | // Add last value |
| 147 | 310 | | if (start < i) |
| | 311 | | { |
| 147 | 312 | | result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString()); |
| | 313 | | } |
| | 314 | |
|
| 147 | 315 | | return result; |
| | 316 | | } |
| | 317 | | } |
| | 318 | | } |