diff options
| author | Cody Robibero <cody@robibe.ro> | 2021-10-26 17:43:36 -0600 |
|---|---|---|
| committer | Cody Robibero <cody@robibe.ro> | 2021-10-26 17:43:36 -0600 |
| commit | f78f1e834ce1907157d4d43cf8564cf40d05fb9f (patch) | |
| tree | beb4e348e4d338a8b459f8a421ba19409d478ba9 /Jellyfin.Server | |
| parent | 2888567ea53c1c839b0cd69e0ec1168cf51f36a8 (diff) | |
| parent | 39d5bdac96b17eb92bd304736cc2728832e1cad0 (diff) | |
Merge remote-tracking branch 'upstream/master' into client-logger
Diffstat (limited to 'Jellyfin.Server')
20 files changed, 529 insertions, 72 deletions
diff --git a/Jellyfin.Server/Configuration/CorsPolicyProvider.cs b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs index 0d04b6bb1..b061be33b 100644 --- a/Jellyfin.Server/Configuration/CorsPolicyProvider.cs +++ b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Server.Configuration } /// <inheritdoc /> - public Task<CorsPolicy> GetPolicyAsync(HttpContext context, string policyName) + public Task<CorsPolicy?> GetPolicyAsync(HttpContext context, string? policyName) { var corsHosts = _serverConfigurationManager.Configuration.CorsHosts; var builder = new CorsPolicyBuilder() @@ -43,7 +43,7 @@ namespace Jellyfin.Server.Configuration .AllowCredentials(); } - return Task.FromResult(builder.Build()); + return Task.FromResult<CorsPolicy?>(builder.Build()); } } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 94c3ca4a9..21bd9ba01 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -9,14 +9,18 @@ using Jellyfin.Api.WebSocketListeners; using Jellyfin.Drawing.Skia; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; +using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Events; +using Jellyfin.Server.Implementations.Security; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; using MediaBrowser.Controller.BaseItemManager; +using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; using Microsoft.EntityFrameworkCore; @@ -74,7 +78,9 @@ namespace Jellyfin.Server } ServiceCollection.AddDbContextPool<JellyfinDb>( - options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}")); + options => options + .UseLoggerFactory(LoggerFactory) + .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}")); ServiceCollection.AddEventServices(); ServiceCollection.AddSingleton<IBaseItemManager, BaseItemManager>(); @@ -84,6 +90,7 @@ namespace Jellyfin.Server ServiceCollection.AddSingleton<IActivityManager, ActivityManager>(); ServiceCollection.AddSingleton<IUserManager, UserManager>(); ServiceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>(); + ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>(); // TODO search the assemblies instead of adding them manually? ServiceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>(); @@ -91,6 +98,10 @@ namespace Jellyfin.Server ServiceCollection.AddSingleton<IWebSocketListener, ScheduledTasksWebSocketListener>(); ServiceCollection.AddSingleton<IWebSocketListener, SessionInfoWebSocketListener>(); + ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>(); + + ServiceCollection.AddScoped<IAuthenticationManager, AuthenticationManager>(); + base.RegisterServices(); } diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 88e2b4152..e29167747 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -79,6 +79,16 @@ namespace Jellyfin.Server.Extensions } /// <summary> + /// Enables url decoding before binding to the application pipeline. + /// </summary> + /// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param> + /// <returns>The updated application builder.</returns> + public static IApplicationBuilder UseQueryStringDecoding(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware<QueryStringDecodingMiddleware>(); + } + + /// <summary> /// Adds base url redirection to the application pipeline. /// </summary> /// <param name="appBuilder">The application builder.</param> diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 924b250ce..e853609d6 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using System.Net.Sockets; using System.Reflection; using Emby.Server.Implementations; using Jellyfin.Api.Auth; +using Jellyfin.Api.Auth.AnonymousLanAccessPolicy; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.DownloadPolicy; using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy; @@ -21,11 +22,11 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Extensions.Json; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; using Jellyfin.Server.Formatters; -using MediaBrowser.Common.Json; using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; @@ -61,6 +62,7 @@ namespace Jellyfin.Server.Extensions serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeOrIgnoreParentalControlSetupHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>(); + serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>(); @@ -157,6 +159,13 @@ namespace Jellyfin.Server.Extensions policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); }); + options.AddPolicy( + Policies.AnonymousLanAccessPolicy, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new AnonymousLanAccessRequirement()); + }); }); } @@ -188,7 +197,8 @@ namespace Jellyfin.Server.Extensions // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues. - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; + if (config.KnownProxies.Length == 0) { options.KnownNetworks.Clear(); @@ -278,7 +288,7 @@ namespace Jellyfin.Server.Extensions { Type = SecuritySchemeType.ApiKey, In = ParameterLocation.Header, - Name = "X-Emby-Authorization", + Name = "Authorization", Description = "API key header parameter" }); @@ -303,7 +313,7 @@ namespace Jellyfin.Server.Extensions { description.TryGetMethodInfo(out MethodInfo methodInfo); // Attribute name, method name, none. - return description?.ActionDescriptor?.AttributeRouteInfo?.Name + return description?.ActionDescriptor.AttributeRouteInfo?.Name ?? methodInfo?.Name ?? null; }); @@ -341,7 +351,7 @@ namespace Jellyfin.Server.Extensions { foreach (var address in host.GetAddresses()) { - AddIpAddress(config, options, addr.Address, addr.PrefixLength); + AddIpAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); } } } @@ -397,7 +407,7 @@ namespace Jellyfin.Server.Extensions Type = "object", Properties = typeof(ImageType).GetEnumNames().ToDictionary( name => name, - name => new OpenApiSchema + _ => new OpenApiSchema { Type = "object", AdditionalProperties = new OpenApiSchema diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs index c349e3dca..ea8c5ecdb 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs index 0480f5e0e..03ca7dda7 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -1,5 +1,5 @@ using System.Net.Mime; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs new file mode 100644 index 000000000..73a619b8d --- /dev/null +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -0,0 +1,144 @@ +// The MIT License (MIT) +// +// Copyright (c) .NET Foundation and Contributors +// +// All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Server.Infrastructure +{ + /// <inheritdoc /> + public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor + { + /// <summary> + /// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class. + /// </summary> + /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param> + public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory) + { + } + + /// <inheritdoc /> + protected override FileMetadata GetFileInfo(string path) + { + var fileInfo = new FileInfo(path); + var length = fileInfo.Length; + // This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371 + if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) + { + using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + length = RandomAccess.GetLength(fileHandle); + } + + return new FileMetadata + { + Exists = fileInfo.Exists, + Length = length, + LastModified = fileInfo.LastWriteTimeUtc + }; + } + + /// <inheritdoc /> + protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + if (range != null && rangeLength == 0) + { + return Task.CompletedTask; + } + + // It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code + if (!IsSymLink(result.FileName)) + { + return base.WriteFileAsync(context, result, range, rangeLength); + } + + var response = context.HttpContext.Response; + + if (range != null) + { + return SendFileAsync( + result.FileName, + response, + offset: range.From ?? 0L, + count: rangeLength); + } + + return SendFileAsync( + result.FileName, + response, + offset: 0, + count: null); + } + + private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count) + { + var fileInfo = GetFileInfo(filePath); + if (offset < 0 || offset > fileInfo.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); + } + + if (count.HasValue + && (count.Value < 0 || count.Value > fileInfo.Length - offset)) + { + throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); + } + + // Copied from SendFileFallback.SendFileAsync + const int BufferSize = 1024 * 16; + + await using var fileStream = new FileStream( + filePath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + bufferSize: BufferSize, + options: FileOptions.Asynchronous | FileOptions.SequentialScan); + + fileStream.Seek(offset, SeekOrigin.Begin); + await StreamCopyOperation + .CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None) + .ConfigureAwait(true); + } + + private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint; + } +} diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index ef842ee26..cb3ff5508 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -8,15 +8,10 @@ <PropertyGroup> <AssemblyName>jellyfin</AssemblyName> <OutputType>Exe</OutputType> - <TargetFramework>net5.0</TargetFramework> + <TargetFramework>net6.0</TargetFramework> <ServerGarbageCollection>false</ServerGarbageCollection> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - <TreatWarningsAsErrors>true</TreatWarningsAsErrors> - <Nullable>enable</Nullable> - <AnalysisMode>AllEnabledByDefault</AnalysisMode> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - <!-- <DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers> --> </PropertyGroup> <ItemGroup> @@ -36,21 +31,21 @@ <ItemGroup> <PackageReference Include="CommandLineParser" Version="2.8.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.5" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.5" /> - <PackageReference Include="prometheus-net" Version="4.1.1" /> - <PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.0-rc.2*" /> + <PackageReference Include="prometheus-net" Version="5.0.1" /> + <PackageReference Include="prometheus-net.AspNetCore" Version="5.0.1" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> - <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" /> - <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" /> - <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> - <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> + <PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" /> + <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" /> + <PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" /> + <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.2" /> <PackageReference Include="Serilog.Sinks.Map" Version="1.0.2" /> - <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.4" /> + <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.6" /> </ItemGroup> <ItemGroup> diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs index c23da2fd6..3e5982eed 100644 --- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs @@ -45,15 +45,36 @@ namespace Jellyfin.Server.Middleware var localPath = httpContext.Request.Path.ToString(); var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl; - if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) - || string.IsNullOrEmpty(localPath) - || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(baseUrlPrefix)) { - // Always redirect back to the default path if the base prefix is invalid or missing + var startsWithBaseUrl = localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase); + + if (!startsWithBaseUrl + && (localPath.Equals("/health", StringComparison.OrdinalIgnoreCase) + || localPath.Equals("/health/", StringComparison.OrdinalIgnoreCase))) + { + _logger.LogDebug("Redirecting /health check"); + httpContext.Response.Redirect(baseUrlPrefix + "/health"); + return; + } + + if (!startsWithBaseUrl + || localPath.Length == baseUrlPrefix.Length + // Local path is /baseUrl/ + || (localPath.Length == baseUrlPrefix.Length + 1 && localPath[^1] == '/')) + { + // Always redirect back to the default path if the base prefix is invalid, missing, or is the full path. + _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); + httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]); + return; + } + } + else if (string.IsNullOrEmpty(localPath) + || localPath.Equals("/", StringComparison.Ordinal)) + { + // Always redirect back to the default path if root is requested. _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); - httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]); + httpContext.Response.Redirect("/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]); return; } diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index f6c76e4d9..db7877c31 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -137,11 +137,6 @@ namespace Jellyfin.Server.Middleware private string NormalizeExceptionMessage(string msg) { - if (msg == null) - { - return string.Empty; - } - // Strip any information we don't want to reveal return msg.Replace( _configuration.ApplicationPaths.ProgramSystemPath, diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs new file mode 100644 index 000000000..cdd86e28e --- /dev/null +++ b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// URL decodes the querystring before binding. + /// </summary> + public class QueryStringDecodingMiddleware + { + private readonly RequestDelegate _next; + + /// <summary> + /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + public QueryStringDecodingMiddleware(RequestDelegate next) + { + _next = next; + } + + /// <summary> + /// Executes the middleware action. + /// </summary> + /// <param name="httpContext">The current HTTP context.</param> + /// <returns>The async task.</returns> + public async Task Invoke(HttpContext httpContext) + { + var feature = httpContext.Features.Get<IQueryFeature>(); + if (feature != null) + { + httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(feature)); + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs new file mode 100644 index 000000000..e4d2937e7 --- /dev/null +++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jellyfin.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// Defines the <see cref="UrlDecodeQueryFeature"/>. + /// </summary> + public class UrlDecodeQueryFeature : IQueryFeature + { + private IQueryCollection? _store; + + /// <summary> + /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class. + /// </summary> + /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param> + public UrlDecodeQueryFeature(IQueryFeature feature) + { + Query = feature.Query; + } + + /// <summary> + /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>. + /// </summary> + public IQueryCollection Query + { + get + { + return _store ?? QueryCollection.Empty; + } + + set + { + // Only interested in where the querystring is encoded which shows up as one key with nothing in the value. + if (value.Count != 1) + { + _store = value; + return; + } + + // Encoded querystrings have no value, so don't process anything if a value is present. + var (key, stringValues) = value.First(); + if (!string.IsNullOrEmpty(stringValues)) + { + _store = value; + return; + } + + if (!key.Contains('=')) + { + _store = value; + return; + } + + var pairs = new Dictionary<string, StringValues>(); + foreach (var pair in key.SpanSplit('&')) + { + var i = pair.IndexOf('='); + if (i == -1) + { + // encoded is an equals. + // We use TryAdd so duplicate keys get ignored + pairs.TryAdd(pair.ToString(), StringValues.Empty); + continue; + } + + var k = pair[..i].ToString(); + var v = pair[(i + 1)..].ToString(); + if (!pairs.TryAdd(k, new StringValues(v))) + { + pairs[k] = StringValues.Concat(pairs[k], v); + } + } + + _store = new QueryCollection(pairs); + } + } + } +} diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index cf938ab8c..7365c8dbc 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -25,7 +25,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.ReaddDefaultPluginRepository), typeof(Routines.MigrateDisplayPreferencesDb), typeof(Routines.RemoveDownloadImagesInAdvance), - typeof(Routines.AddPeopleQueryIndex) + typeof(Routines.AddPeopleQueryIndex), + typeof(Routines.MigrateAuthenticationDb) }; /// <summary> @@ -40,7 +41,7 @@ namespace Jellyfin.Server.Migrations .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m)) .OfType<IMigrationRoutine>() .ToArray(); - var migrationOptions = ((IConfigurationManager)host.ConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey); + var migrationOptions = host.ConfigurationManager.GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey); if (!host.ConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0) { diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs index 6821630db..ee4f8b0ba 100644 --- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs +++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs @@ -75,7 +75,7 @@ namespace Jellyfin.Server.Migrations.Routines { var existingConfigJson = JToken.Parse(File.ReadAllText(oldConfigPath)); return _defaultConfigHistory - .Select(historicalConfigText => JToken.Parse(historicalConfigText)) + .Select(JToken.Parse) .Any(historicalConfigJson => JToken.DeepEquals(existingConfigJson, historicalConfigJson)); } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index 6048160c6..9e22978ae 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -92,7 +92,7 @@ namespace Jellyfin.Server.Migrations.Routines if (entry[6].SQLiteType != SQLiteType.Null && !Guid.TryParse(entry[6].ToString(), out guid)) { // This is not a valid Guid, see if it is an internal ID from an old Emby schema - _logger.LogWarning("Invalid Guid in UserId column: ", entry[6].ToString()); + _logger.LogWarning("Invalid Guid in UserId column: {Guid}", entry[6].ToString()); using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id"); statement.TryBind("@Id", entry[6].ToString()); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs new file mode 100644 index 000000000..21f153623 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Emby.Server.Implementations.Data; +using Jellyfin.Data.Entities.Security; +using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// A migration that moves data from the authentication database into the new schema. + /// </summary> + public class MigrateAuthenticationDb : IMigrationRoutine + { + private const string DbFilename = "authentication.db"; + + private readonly ILogger<MigrateAuthenticationDb> _logger; + private readonly JellyfinDbProvider _dbProvider; + private readonly IServerApplicationPaths _appPaths; + + /// <summary> + /// Initializes a new instance of the <see cref="MigrateAuthenticationDb"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="dbProvider">The database provider.</param> + /// <param name="appPaths">The server application paths.</param> + public MigrateAuthenticationDb(ILogger<MigrateAuthenticationDb> logger, JellyfinDbProvider dbProvider, IServerApplicationPaths appPaths) + { + _logger = logger; + _dbProvider = dbProvider; + _appPaths = appPaths; + } + + /// <inheritdoc /> + public Guid Id => Guid.Parse("5BD72F41-E6F3-4F60-90AA-09869ABE0E22"); + + /// <inheritdoc /> + public string Name => "MigrateAuthenticationDatabase"; + + /// <inheritdoc /> + public bool PerformOnNewInstall => false; + + /// <inheritdoc /> + public void Perform() + { + var dataPath = _appPaths.DataPath; + using (var connection = SQLite3.Open( + Path.Combine(dataPath, DbFilename), + ConnectionFlags.ReadOnly, + null)) + { + using var dbContext = _dbProvider.CreateContext(); + + var authenticatedDevices = connection.Query("SELECT * FROM Tokens"); + + foreach (var row in authenticatedDevices) + { + if (row[6].IsDbNull()) + { + dbContext.ApiKeys.Add(new ApiKey(row[3].ToString()) + { + AccessToken = row[1].ToString(), + DateCreated = row[9].ToDateTime(), + DateLastActivity = row[10].ToDateTime() + }); + } + else + { + dbContext.Devices.Add(new Device( + new Guid(row[6].ToString()), + row[3].ToString(), + row[4].ToString(), + row[5].ToString(), + row[2].ToString()) + { + AccessToken = row[1].ToString(), + IsActive = row[8].ToBool(), + DateCreated = row[9].ToDateTime(), + DateLastActivity = row[10].ToDateTime() + }); + } + } + + var deviceOptions = connection.Query("SELECT * FROM Devices"); + var deviceIds = new HashSet<string>(); + foreach (var row in deviceOptions) + { + if (row[2].IsDbNull()) + { + continue; + } + + var deviceId = row[2].ToString(); + if (deviceIds.Contains(deviceId)) + { + continue; + } + + deviceIds.Add(deviceId); + + dbContext.DeviceOptions.Add(new DeviceOptions(deviceId) + { + CustomName = row[1].IsDbNull() ? null : row[1].ToString() + }); + } + + dbContext.SaveChanges(); + } + + try + { + File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old")); + + var journalPath = Path.Combine(dataPath, DbFilename + "-journal"); + if (File.Exists(journalPath)) + { + File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal")); + } + } + catch (IOException e) + { + _logger.LogError(e, "Error renaming legacy activity log database to 'authentication.db.old'"); + } + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index e25d29122..40f871759 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Text.Json; @@ -121,7 +120,7 @@ namespace Jellyfin.Server.Migrations.Routines var displayPreferences = new DisplayPreferences(dtoUserId, itemId, client) { - IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null, + IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : null, ShowBackdrop = dto.ShowBackdrop, ShowSidebar = dto.ShowSidebar, ScrollDirection = dto.ScrollDirection, diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index a15a38177..9b2d603c7 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -1,15 +1,15 @@ using System; using System.IO; using Emby.Server.Implementations.Data; -using Emby.Server.Implementations.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions.Json; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Users; -using MediaBrowser.Common.Json; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -27,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines private readonly ILogger<MigrateUserDb> _logger; private readonly IServerApplicationPaths _paths; private readonly JellyfinDbProvider _provider; - private readonly MyXmlSerializer _xmlSerializer; + private readonly IXmlSerializer _xmlSerializer; /// <summary> /// Initializes a new instance of the <see cref="MigrateUserDb"/> class. @@ -40,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines ILogger<MigrateUserDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider, - MyXmlSerializer xmlSerializer) + IXmlSerializer xmlSerializer) { _logger = logger; _paths = paths; @@ -82,11 +82,14 @@ namespace Jellyfin.Server.Migrations.Routines var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name); - var config = File.Exists(Path.Combine(userDataDir, "config.xml")) - ? (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml")) + var configPath = Path.Combine(userDataDir, "config.xml"); + var config = File.Exists(configPath) + ? (UserConfiguration?)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), configPath) ?? new UserConfiguration() : new UserConfiguration(); - var policy = File.Exists(Path.Combine(userDataDir, "policy.xml")) - ? (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml")) + + var policyPath = Path.Combine(userDataDir, "policy.xml"); + var policy = File.Exists(policyPath) + ? (UserPolicy?)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), policyPath) ?? new UserPolicy() : new UserPolicy(); policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace( "Emby.Server.Implementations.Library", diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 7a7e0a1cf..778e53cf6 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -5,18 +5,20 @@ using System.IO; using System.Linq; using System.Net; using System.Reflection; -using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; +using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -121,11 +123,11 @@ namespace Jellyfin.Server // Log uncaught exceptions to the logging instead of std error AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole; - AppDomain.CurrentDomain.UnhandledException += (sender, e) + AppDomain.CurrentDomain.UnhandledException += (_, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); // Intercept Ctrl+C and Ctrl+Break - Console.CancelKeyPress += (sender, e) => + Console.CancelKeyPress += (_, e) => { if (_tokenSource.IsCancellationRequested) { @@ -139,7 +141,7 @@ namespace Jellyfin.Server }; // Register a SIGTERM handler - AppDomain.CurrentDomain.ProcessExit += (sender, e) => + AppDomain.CurrentDomain.ProcessExit += (_, _) => { if (_tokenSource.IsCancellationRequested) { @@ -180,7 +182,7 @@ namespace Jellyfin.Server "The server is expected to host the web client, but the provided content directory is either " + $"invalid or empty: {webContentPath}. If you do not want to host the web client with the " + "server, you may set the '--nowebclient' command line flag, or set" + - $"'{MediaBrowser.Controller.Extensions.ConfigurationExtensions.HostWebClientKey}=false' in your config settings."); + $"'{ConfigurationExtensions.HostWebClientKey}=false' in your config settings."); } } @@ -195,9 +197,9 @@ namespace Jellyfin.Server try { - await webHost.StartAsync().ConfigureAwait(false); + await webHost.StartAsync(_tokenSource.Token).ConfigureAwait(false); } - catch + catch (Exception ex) when (ex is not TaskCanceledException) { _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again."); throw; @@ -222,6 +224,14 @@ namespace Jellyfin.Server } finally { + _logger.LogInformation("Running query planner optimizations in the database... This might take a while"); + // Run before disposing the application + using var context = appHost.Resolve<JellyfinDbProvider>().CreateContext(); + if (context.Database.IsSqlite()) + { + context.Database.ExecuteSqlRaw("PRAGMA optimize"); + } + appHost.Dispose(); } @@ -310,8 +320,8 @@ namespace Jellyfin.Server } } - // Bind to unix socket (only on macOS and Linux) - if (startupConfig.UseUnixSocket() && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + // Bind to unix socket (only on unix systems) + if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) { var socketPath = startupConfig.GetUnixSocketPath(); if (string.IsNullOrEmpty(socketPath)) @@ -396,7 +406,7 @@ namespace Jellyfin.Server { if (options.DataDir != null || Directory.Exists(Path.Combine(dataDir, "config")) - || RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + || OperatingSystem.IsWindows()) { // Hang config folder off already set dataDir configDir = Path.Combine(dataDir, "config"); @@ -434,7 +444,7 @@ namespace Jellyfin.Server if (string.IsNullOrEmpty(cacheDir)) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (OperatingSystem.IsWindows()) { // Hang cache folder off already set dataDir cacheDir = Path.Combine(dataDir, "cache"); @@ -535,11 +545,11 @@ namespace Jellyfin.Server // Get a stream of the resource contents // NOTE: The .csproj name is used instead of the assembly name in the resource path const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; - await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) + await using Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); // Copy the resource contents to the expected file path for the config file - await using Stream dst = File.Open(configPath, FileMode.CreateNew); + await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await resource.CopyToAsync(dst).ConfigureAwait(false); } @@ -586,15 +596,14 @@ namespace Jellyfin.Server try { // Serilog.Log is used by SerilogLoggerFactory when no logger is specified - Serilog.Log.Logger = new LoggerConfiguration() + Log.Logger = new LoggerConfiguration() .WriteTo.Logger(lc => lc.ReadFrom.Configuration(configuration) .Enrich.FromLogContext() .Enrich.WithThreadId() .Filter.ByExcluding(Matching.FromSource<ClientEventLogger>())) .WriteTo.Logger(lc => - lc - .WriteTo.Map( + lc.WriteTo.Map( "ClientName", (clientName, wt) => wt.File( @@ -607,7 +616,7 @@ namespace Jellyfin.Server } catch (Exception ex) { - Serilog.Log.Logger = new LoggerConfiguration() + Log.Logger = new LoggerConfiguration() .WriteTo.Logger(lc => lc.WriteTo.Async(x => x.File( Path.Combine(appPaths.LogDirectoryPath, "log_.log"), @@ -630,7 +639,7 @@ namespace Jellyfin.Server .Filter.ByIncludingOnly(Matching.FromSource<ClientEventLogger>())) .CreateLogger(); - Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); + Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index f75139884..8085c2630 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -7,6 +7,7 @@ using System.Text; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; +using Jellyfin.Server.Infrastructure; using Jellyfin.Server.Middleware; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -14,6 +15,8 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -56,6 +59,9 @@ namespace Jellyfin.Server { options.HttpsPort = _serverApplicationHost.HttpsPort; }); + + // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371 + services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>(); services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration()); services.AddJellyfinApiSwagger(); @@ -160,6 +166,7 @@ namespace Jellyfin.Server mainApp.UseAuthentication(); mainApp.UseJellyfinApiSwagger(_serverConfigurationManager); + mainApp.UseQueryStringDecoding(); mainApp.UseRouting(); mainApp.UseAuthorization(); |
