aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server')
-rw-r--r--Jellyfin.Server/Configuration/CorsPolicyProvider.cs4
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs5
-rw-r--r--Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs144
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj12
-rw-r--r--Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs6
-rw-r--r--Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs11
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs6
-rw-r--r--Jellyfin.Server/Program.cs12
-rw-r--r--Jellyfin.Server/Startup.cs6
10 files changed, 178 insertions, 30 deletions
diff --git a/Jellyfin.Server/Configuration/CorsPolicyProvider.cs b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
index 0d04b6bb13..b061be33b5 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/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index b5444634b4..e853609d69 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -197,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();
@@ -287,7 +288,7 @@ namespace Jellyfin.Server.Extensions
{
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
- Name = "X-Emby-Authorization",
+ Name = "Authorization",
Description = "API key header parameter"
});
diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
new file mode 100644
index 0000000000..73a619b8d9
--- /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 1fdad73b74..8983eb50fd 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -8,7 +8,7 @@
<PropertyGroup>
<AssemblyName>jellyfin</AssemblyName>
<OutputType>Exe</OutputType>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<ServerGarbageCollection>false</ServerGarbageCollection>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -31,15 +31,15 @@
<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.9" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.9" />
+ <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.2.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" />
diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
index fd0ebbf438..cdd86e28e6 100644
--- a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
+++ b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
@@ -27,7 +27,11 @@ namespace Jellyfin.Server.Middleware
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
- httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(httpContext.Features.Get<IQueryFeature>()));
+ 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
index c1f5b5dfaf..e4d2937e7e 100644
--- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
+++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Web;
using Jellyfin.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
@@ -52,20 +51,14 @@ namespace Jellyfin.Server.Middleware
return;
}
- // Unencode and re-parse querystring.
- var unencodedKey = HttpUtility.UrlDecode(key);
-
- if (string.Equals(unencodedKey, key, StringComparison.Ordinal))
+ if (!key.Contains('='))
{
- // Don't do anything if it's not encoded.
_store = value;
return;
}
var pairs = new Dictionary<string, StringValues>();
- var queryString = unencodedKey.SpanSplit('&');
-
- foreach (var pair in queryString)
+ foreach (var pair in key.SpanSplit('&'))
{
var i = pair.IndexOf('=');
if (i == -1)
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 6ff59626de..40f871759c 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -120,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 d9524645a1..9b2d603c7f 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -1,7 +1,6 @@
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;
@@ -10,6 +9,7 @@ using Jellyfin.Server.Implementations.Users;
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;
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 3c0ee069d4..45699f3aff 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -195,9 +195,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;
@@ -547,7 +547,7 @@ namespace Jellyfin.Server
?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
// Copy the resource contents to the expected file path for the config file
- await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
+ await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await resource.CopyToAsync(dst).ConfigureAwait(false);
}
@@ -594,7 +594,7 @@ namespace Jellyfin.Server
try
{
// Serilog.Log is used by SerilogLoggerFactory when no logger is specified
- Serilog.Log.Logger = new LoggerConfiguration()
+ Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
.Enrich.WithThreadId()
@@ -602,7 +602,7 @@ namespace Jellyfin.Server
}
catch (Exception ex)
{
- Serilog.Log.Logger = new LoggerConfiguration()
+ Log.Logger = new LoggerConfiguration()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
.WriteTo.Async(x => x.File(
Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
@@ -613,7 +613,7 @@ namespace Jellyfin.Server
.Enrich.WithThreadId()
.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 60cdc2f6fe..8085c26308 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();