aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server')
-rw-r--r--Jellyfin.Server/CoreAppHost.cs27
-rw-r--r--Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs51
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs21
-rw-r--r--Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs36
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj5
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs62
-rw-r--r--Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs76
-rw-r--r--Jellyfin.Server/Middleware/LanFilteringMiddleware.cs76
-rw-r--r--Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs49
-rw-r--r--Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs40
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs5
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs13
-rw-r--r--Jellyfin.Server/Program.cs11
-rw-r--r--Jellyfin.Server/Startup.cs62
14 files changed, 498 insertions, 36 deletions
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 29a59e1c8..755844dd9 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -1,20 +1,20 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Reflection;
using Emby.Drawing;
using Emby.Server.Implementations;
using Jellyfin.Drawing.Skia;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
+using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
-using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -33,30 +33,33 @@ namespace Jellyfin.Server
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
+ /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public CoreAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IFileSystem fileSystem,
- INetworkManager networkManager)
+ INetworkManager networkManager,
+ IServiceCollection collection)
: base(
applicationPaths,
loggerFactory,
options,
fileSystem,
- networkManager)
+ networkManager,
+ collection)
{
}
/// <inheritdoc/>
- protected override void RegisterServices(IServiceCollection serviceCollection)
+ protected override void RegisterServices()
{
// Register an image encoder
bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable();
Type imageEncoderType = useSkiaEncoder
? typeof(SkiaEncoder)
: typeof(NullImageEncoder);
- serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
+ ServiceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
// Log a warning if the Skia encoder could not be used
if (!useSkiaEncoder)
@@ -71,13 +74,15 @@ namespace Jellyfin.Server
// .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
// ServiceLifetime.Transient);
- serviceCollection.AddSingleton<JellyfinDbProvider>();
+ ServiceCollection.AddEventServices();
+ ServiceCollection.AddSingleton<IEventManager, EventManager>();
+ ServiceCollection.AddSingleton<JellyfinDbProvider>();
- serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
- serviceCollection.AddSingleton<IUserManager, UserManager>();
- serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
+ ServiceCollection.AddSingleton<IActivityManager, ActivityManager>();
+ ServiceCollection.AddSingleton<IUserManager, UserManager>();
+ ServiceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
- base.RegisterServices(serviceCollection);
+ base.RegisterServices();
}
/// <inheritdoc />
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 745567703..71c66a310 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Server.Middleware;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
@@ -46,5 +47,55 @@ namespace Jellyfin.Server.Extensions
c.RoutePrefix = $"{baseUrl}api-docs/redoc";
});
}
+
+ /// <summary>
+ /// Adds IP based access validation to the application pipeline.
+ /// </summary>
+ /// <param name="appBuilder">The application builder.</param>
+ /// <returns>The updated application builder.</returns>
+ public static IApplicationBuilder UseIpBasedAccessValidation(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware<IpBasedAccessValidationMiddleware>();
+ }
+
+ /// <summary>
+ /// Adds LAN based access filtering to the application pipeline.
+ /// </summary>
+ /// <param name="appBuilder">The application builder.</param>
+ /// <returns>The updated application builder.</returns>
+ public static IApplicationBuilder UseLanFiltering(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware<LanFilteringMiddleware>();
+ }
+
+ /// <summary>
+ /// Adds base url redirection to the application pipeline.
+ /// </summary>
+ /// <param name="appBuilder">The application builder.</param>
+ /// <returns>The updated application builder.</returns>
+ public static IApplicationBuilder UseBaseUrlRedirection(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware<BaseUrlRedirectionMiddleware>();
+ }
+
+ /// <summary>
+ /// Adds a custom message during server startup to the application pipeline.
+ /// </summary>
+ /// <param name="appBuilder">The application builder.</param>
+ /// <returns>The updated application builder.</returns>
+ public static IApplicationBuilder UseServerStartupMessage(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware<ServerStartupMessageMiddleware>();
+ }
+
+ /// <summary>
+ /// Adds a WebSocket request handler to the application pipeline.
+ /// </summary>
+ /// <param name="appBuilder">The application builder.</param>
+ /// <returns>The updated application builder.</returns>
+ public static IApplicationBuilder UseWebSocketHandler(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>();
+ }
}
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index c933a298e..bcce19d3c 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -18,6 +18,7 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
using Jellyfin.Server.Formatters;
using Jellyfin.Server.Models;
+using MediaBrowser.Common;
using MediaBrowser.Common.Json;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication;
@@ -135,10 +136,11 @@ namespace Jellyfin.Server.Extensions
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="baseUrl">The base url for the API.</param>
+ /// <param name="pluginAssemblies">An IEnumberable containing all plugin assemblies with API controllers.</param>
/// <returns>The MVC builder.</returns>
- public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl)
+ public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl, IEnumerable<Assembly> pluginAssemblies)
{
- return serviceCollection
+ IMvcBuilder mvcBuilder = serviceCollection
.AddCors(options =>
{
options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
@@ -172,6 +174,9 @@ namespace Jellyfin.Server.Extensions
// From JsonDefaults
options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
+ options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
+ options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
+
options.JsonSerializerOptions.Converters.Clear();
foreach (var converter in jsonOptions.Converters)
{
@@ -180,8 +185,14 @@ namespace Jellyfin.Server.Extensions
// From JsonDefaults.PascalCase
options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy;
- })
- .AddControllersAsServices();
+ });
+
+ foreach (Assembly pluginAssembly in pluginAssemblies)
+ {
+ mvcBuilder.AddApplicationPart(pluginAssembly);
+ }
+
+ return mvcBuilder.AddControllersAsServices();
}
/// <summary>
@@ -198,7 +209,7 @@ namespace Jellyfin.Server.Extensions
{
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
- Name = "X-Emby-Token",
+ Name = "X-Emby-Authorization",
Description = "API key header parameter"
});
diff --git a/Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs b/Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs
new file mode 100644
index 000000000..aea684479
--- /dev/null
+++ b/Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs
@@ -0,0 +1,36 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Server.Implementations;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Jellyfin.Server.HealthChecks
+{
+ /// <summary>
+ /// Checks connectivity to the database.
+ /// </summary>
+ public class JellyfinDbHealthCheck : IHealthCheck
+ {
+ private readonly JellyfinDbProvider _dbProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JellyfinDbHealthCheck"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The jellyfin db provider.</param>
+ public JellyfinDbHealthCheck(JellyfinDbProvider dbProvider)
+ {
+ _dbProvider = dbProvider;
+ }
+
+ /// <inheritdoc />
+ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ await using var jellyfinDb = _dbProvider.CreateContext();
+ if (await jellyfinDb.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false))
+ {
+ return HealthCheckResult.Healthy("Database connection successful.");
+ }
+
+ return HealthCheckResult.Unhealthy("Unable to connect to the database.");
+ }
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 7541707d9..6ca370d04 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -41,8 +41,9 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.6" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.7" />
<PackageReference Include="prometheus-net" Version="3.6.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
new file mode 100644
index 000000000..9316737bd
--- /dev/null
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// Redirect requests without baseurl prefix to the baseurl prefixed URL.
+ /// </summary>
+ public class BaseUrlRedirectionMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ILogger<BaseUrlRedirectionMiddleware> _logger;
+ private readonly IConfiguration _configuration;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="configuration">The application configuration.</param>
+ public BaseUrlRedirectionMiddleware(
+ RequestDelegate next,
+ ILogger<BaseUrlRedirectionMiddleware> logger,
+ IConfiguration configuration)
+ {
+ _next = next;
+ _logger = logger;
+ _configuration = configuration;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager)
+ {
+ var localPath = httpContext.Request.Path.ToString();
+ var baseUrlPrefix = serverConfigurationManager.Configuration.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))
+ {
+ // Always redirect back to the default path if the base prefix is invalid or missing
+ _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
+ httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ return;
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
new file mode 100644
index 000000000..59b5fb1ed
--- /dev/null
+++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
@@ -0,0 +1,76 @@
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// Validates the IP of requests coming from local networks wrt. remote access.
+ /// </summary>
+ public class IpBasedAccessValidationMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public IpBasedAccessValidationMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <param name="networkManager">The network manager.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
+ {
+ if (httpContext.Request.IsLocal())
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ var remoteIp = httpContext.Request.RemoteIp();
+
+ if (serverConfigurationManager.Configuration.EnableRemoteAccess)
+ {
+ var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+
+ if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp))
+ {
+ if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist)
+ {
+ if (networkManager.IsAddressInSubnets(remoteIp, addressFilter))
+ {
+ return;
+ }
+ }
+ else
+ {
+ if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter))
+ {
+ return;
+ }
+ }
+ }
+ }
+ else
+ {
+ if (!networkManager.IsInLocalNetwork(remoteIp))
+ {
+ return;
+ }
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
new file mode 100644
index 000000000..9d795145a
--- /dev/null
+++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// Validates the LAN host IP based on application configuration.
+ /// </summary>
+ public class LanFilteringMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public LanFilteringMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <param name="networkManager">The network manager.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
+ {
+ var currentHost = httpContext.Request.Host.ToString();
+ var hosts = serverConfigurationManager
+ .Configuration
+ .LocalNetworkAddresses
+ .Select(NormalizeConfiguredLocalAddress)
+ .ToList();
+
+ if (hosts.Count == 0)
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ currentHost ??= string.Empty;
+
+ if (networkManager.IsInPrivateAddressSpace(currentHost))
+ {
+ hosts.Add("localhost");
+ hosts.Add("127.0.0.1");
+
+ if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1))
+ {
+ return;
+ }
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+
+ private static string NormalizeConfiguredLocalAddress(string address)
+ {
+ var add = address.AsSpan().Trim('/');
+ int index = add.IndexOf('/');
+ if (index != -1)
+ {
+ add = add.Slice(index + 1);
+ }
+
+ return add.TrimStart('/').ToString();
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
new file mode 100644
index 000000000..ea81c03a2
--- /dev/null
+++ b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
@@ -0,0 +1,49 @@
+using System.Net.Mime;
+using System.Threading.Tasks;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// Shows a custom message during server startup.
+ /// </summary>
+ public class ServerStartupMessageMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public ServerStartupMessageMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <param name="serverApplicationHost">The server application host.</param>
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(
+ HttpContext httpContext,
+ IServerApplicationHost serverApplicationHost,
+ ILocalizationManager localizationManager)
+ {
+ if (serverApplicationHost.CoreStartupHasCompleted)
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
+ httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
+ httpContext.Response.ContentType = MediaTypeNames.Text.Html;
+ await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
new file mode 100644
index 000000000..b7a5d2b34
--- /dev/null
+++ b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
@@ -0,0 +1,40 @@
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// Handles WebSocket requests.
+ /// </summary>
+ public class WebSocketHandlerMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public WebSocketHandlerMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <param name="webSocketManager">The WebSocket connection manager.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager)
+ {
+ if (!httpContext.WebSockets.IsWebSocketRequest)
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
+
+ await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index b15ccf01e..7f57358ec 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -81,6 +81,11 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var result in results)
{
var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToString(), _jsonOptions);
+ if (dto == null)
+ {
+ continue;
+ }
+
var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
? chromecastDict[version]
: ChromecastVersion.Stable;
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 274e6ab73..74c550331 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -1,5 +1,7 @@
using System;
+using System.Globalization;
using System.IO;
+using System.Linq;
using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Serialization;
using Jellyfin.Data.Entities;
@@ -74,7 +76,12 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var entry in queryResult)
{
- UserMockup mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.GetOptions());
+ UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.GetOptions());
+ if (mockup == null)
+ {
+ continue;
+ }
+
var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
var config = File.Exists(Path.Combine(userDataDir, "config.xml"))
@@ -161,9 +168,9 @@ namespace Jellyfin.Server.Migrations.Routines
}
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
- user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+ user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
- user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+ user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index f6ac4e2a3..b9a90f9db 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -11,7 +11,6 @@ using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
-using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Networking;
using Jellyfin.Api.Controllers;
@@ -28,6 +27,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Extensions.Logging;
using SQLitePCL;
+using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Jellyfin.Server
@@ -154,13 +154,15 @@ namespace Jellyfin.Server
ApplicationHost.LogEnvironmentInfo(_logger, appPaths);
PerformStaticInitialization();
+ var serviceCollection = new ServiceCollection();
var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
- new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()));
+ new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
+ serviceCollection);
try
{
@@ -178,8 +180,7 @@ namespace Jellyfin.Server
}
}
- ServiceCollection serviceCollection = new ServiceCollection();
- appHost.Init(serviceCollection);
+ appHost.Init();
var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build();
@@ -593,7 +594,7 @@ namespace Jellyfin.Server
var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
if (startupConfig != null && !startupConfig.HostWebClient())
{
- inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger";
+ inMemoryDefaultConfig[ConfigurationExtensions.DefaultRedirectKey] = "api-docs/swagger";
}
return config
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index d0dd183c6..9e456de12 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,9 +1,12 @@
using System;
using System.ComponentModel;
+using System.Net.Http.Headers;
using Jellyfin.Api.TypeConverters;
using Jellyfin.Server.Extensions;
+using Jellyfin.Server.HealthChecks;
using Jellyfin.Server.Middleware;
using Jellyfin.Server.Models;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
@@ -20,14 +23,19 @@ namespace Jellyfin.Server
public class Startup
{
private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IServerApplicationHost _serverApplicationHost;
/// <summary>
/// Initializes a new instance of the <see cref="Startup" /> class.
/// </summary>
/// <param name="serverConfigurationManager">The server configuration manager.</param>
- public Startup(IServerConfigurationManager serverConfigurationManager)
+ /// <param name="serverApplicationHost">The server application host.</param>
+ public Startup(
+ IServerConfigurationManager serverConfigurationManager,
+ IServerApplicationHost serverApplicationHost)
{
_serverConfigurationManager = serverConfigurationManager;
+ _serverApplicationHost = serverApplicationHost;
}
/// <summary>
@@ -38,7 +46,13 @@ namespace Jellyfin.Server
{
services.AddResponseCompression();
services.AddHttpContextAccessor();
- services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'));
+ services.AddHttpsRedirection(options =>
+ {
+ options.HttpsPort = _serverApplicationHost.HttpsPort;
+ });
+ services.AddJellyfinApi(
+ _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'),
+ _serverApplicationHost.GetApiPluginAssemblies());
services.AddJellyfinApiSwagger();
@@ -46,7 +60,26 @@ namespace Jellyfin.Server
services.AddCustomAuthentication();
services.AddJellyfinApiAuthorization();
- services.AddHttpClient();
+
+ var productHeader = new ProductInfoHeaderValue(
+ _serverApplicationHost.Name.Replace(' ', '-'),
+ _serverApplicationHost.ApplicationVersionString);
+ services
+ .AddHttpClient(NamedClient.Default, c =>
+ {
+ c.DefaultRequestHeaders.UserAgent.Add(productHeader);
+ })
+ .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
+
+ services.AddHttpClient(NamedClient.MusicBrainz, c =>
+ {
+ c.DefaultRequestHeaders.UserAgent.Add(productHeader);
+ c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})"));
+ })
+ .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
+
+ services.AddHealthChecks()
+ .AddCheck<JellyfinDbHealthCheck>("JellyfinDb");
}
/// <summary>
@@ -54,11 +87,9 @@ namespace Jellyfin.Server
/// </summary>
/// <param name="app">The application builder.</param>
/// <param name="env">The webhost environment.</param>
- /// <param name="serverApplicationHost">The server application host.</param>
public void Configure(
IApplicationBuilder app,
- IWebHostEnvironment env,
- IServerApplicationHost serverApplicationHost)
+ IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
@@ -73,12 +104,17 @@ namespace Jellyfin.Server
app.UseResponseCompression();
- // TODO app.UseMiddleware<WebSocketMiddleware>();
+ app.UseCors(ServerCorsPolicy.DefaultPolicyName);
+
+ if (_serverConfigurationManager.Configuration.RequireHttps
+ && _serverApplicationHost.ListenWithHttps)
+ {
+ app.UseHttpsRedirection();
+ }
app.UseAuthentication();
app.UseJellyfinApiSwagger(_serverConfigurationManager);
app.UseRouting();
- app.UseCors(ServerCorsPolicy.DefaultPolicyName);
app.UseAuthorization();
if (_serverConfigurationManager.Configuration.EnableMetrics)
{
@@ -86,6 +122,12 @@ namespace Jellyfin.Server
app.UseHttpMetrics();
}
+ app.UseLanFiltering();
+ app.UseIpBasedAccessValidation();
+ app.UseBaseUrlRedirection();
+ app.UseWebSocketHandler();
+ app.UseServerStartupMessage();
+
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
@@ -93,9 +135,9 @@ namespace Jellyfin.Server
{
endpoints.MapMetrics(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/metrics");
}
- });
- app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
+ endpoints.MapHealthChecks(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/health");
+ });
// Add type descriptor for legacy datetime parsing.
TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));