aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server
diff options
context:
space:
mode:
authorCody Robibero <cody@robibe.ro>2021-10-26 17:43:36 -0600
committerCody Robibero <cody@robibe.ro>2021-10-26 17:43:36 -0600
commitf78f1e834ce1907157d4d43cf8564cf40d05fb9f (patch)
treebeb4e348e4d338a8b459f8a421ba19409d478ba9 /Jellyfin.Server
parent2888567ea53c1c839b0cd69e0ec1168cf51f36a8 (diff)
parent39d5bdac96b17eb92bd304736cc2728832e1cad0 (diff)
Merge remote-tracking branch 'upstream/master' into client-logger
Diffstat (limited to 'Jellyfin.Server')
-rw-r--r--Jellyfin.Server/Configuration/CorsPolicyProvider.cs4
-rw-r--r--Jellyfin.Server/CoreAppHost.cs13
-rw-r--r--Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs10
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs22
-rw-r--r--Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs2
-rw-r--r--Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs2
-rw-r--r--Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs144
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj29
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs35
-rw-r--r--Jellyfin.Server/Middleware/ExceptionMiddleware.cs5
-rw-r--r--Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs39
-rw-r--r--Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs84
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs5
-rw-r--r--Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs129
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs19
-rw-r--r--Jellyfin.Server/Program.cs45
-rw-r--r--Jellyfin.Server/Startup.cs7
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();