aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server')
-rw-r--r--Jellyfin.Server/CoreAppHost.cs13
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs2
-rw-r--r--Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs145
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj6
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs129
-rw-r--r--Jellyfin.Server/Program.cs9
-rw-r--r--Jellyfin.Server/Startup.cs6
8 files changed, 303 insertions, 10 deletions
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 94c3ca4a95..21bd9ba011 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/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index f19e87aba5..183480487a 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -278,7 +278,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..e171fc145c
--- /dev/null
+++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
@@ -0,0 +1,145 @@
+// 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 MediaBrowser.Model.IO;
+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 Stream thisFileStream = File.OpenRead(path);
+ length = thisFileStream.Length;
+ }
+
+ 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: (AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | 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 ea64663bd7..1fdad73b74 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -35,8 +35,8 @@
<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="prometheus-net" Version="4.2.0" />
- <PackageReference Include="prometheus-net.AspNetCore" Version="4.2.0" />
+ <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" />
@@ -44,7 +44,7 @@
<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="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/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 0af5cfd619..7365c8dbc1 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>
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
new file mode 100644
index 0000000000..21f153623e
--- /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/Program.cs b/Jellyfin.Server/Program.cs
index 7018d537fd..1300ce3b67 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -15,6 +15,7 @@ using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -194,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;
@@ -223,7 +224,7 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Running query planner optimizations in the database... This might take a while");
// Run before disposing the application
- using var context = new JellyfinDbProvider(appHost.ServiceProvider, appPaths).CreateContext();
+ using var context = appHost.Resolve<JellyfinDbProvider>().CreateContext();
if (context.Database.IsSqlite())
{
context.Database.ExecuteSqlRaw("PRAGMA optimize");
@@ -546,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 = File.Open(configPath, FileMode.CreateNew);
+ await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO);
await resource.CopyToAsync(dst).ConfigureAwait(false);
}
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();