diff options
| author | JPVenson <github@jpb.email> | 2025-04-19 22:08:24 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-19 13:08:24 -0600 |
| commit | 7df6e0b16f8e6b3026955e31417906b3bcbba290 (patch) | |
| tree | 1136edfe7f28e47b954e2bfa3963af256d597911 /Jellyfin.Server | |
| parent | 269508be9f78901b3a3b2bea88392aeef88359e4 (diff) | |
Add port awareness to startup server (#13913)
Diffstat (limited to 'Jellyfin.Server')
| -rw-r--r-- | Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs | 116 | ||||
| -rw-r--r-- | Jellyfin.Server/Program.cs | 16 | ||||
| -rw-r--r-- | Jellyfin.Server/ServerSetupApp/SetupServer.cs | 74 |
3 files changed, 150 insertions, 56 deletions
diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index 7695c0d9e..be9cf0f15 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -1,10 +1,14 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; +using System.Security.Cryptography.X509Certificates; using Jellyfin.Server.Helpers; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -35,56 +39,98 @@ public static class WebHostBuilderExtensions return builder .UseKestrel((builderContext, options) => { - var addresses = appHost.NetManager.GetAllBindInterfaces(false); + SetupJellyfinWebServer( + appHost.NetManager.GetAllBindInterfaces(false), + appHost.HttpPort, + appHost.ListenWithHttps ? appHost.HttpsPort : null, + appHost.Certificate, + startupConfig, + appPaths, + logger, + builderContext, + options); + }) + .UseStartup(context => new Startup(appHost, context.Configuration)); + } - bool flagged = false; - foreach (var netAdd in addresses) + /// <summary> + /// Configures a Kestrel type webServer to bind to the specific arguments. + /// </summary> + /// <param name="addresses">The IP addresses that should be listend to.</param> + /// <param name="httpPort">The http port.</param> + /// <param name="httpsPort">If set the https port. If set you must also set the certificate.</param> + /// <param name="certificate">The certificate used for https port.</param> + /// <param name="startupConfig">The startup config.</param> + /// <param name="appPaths">The app paths.</param> + /// <param name="logger">A logger.</param> + /// <param name="builderContext">The kestrel build pipeline context.</param> + /// <param name="options">The kestrel server options.</param> + /// <exception cref="InvalidOperationException">Will be thrown when a https port is set but no or an invalid certificate is provided.</exception> + public static void SetupJellyfinWebServer( + IReadOnlyList<IPData> addresses, + int httpPort, + int? httpsPort, + X509Certificate2? certificate, + IConfiguration startupConfig, + IApplicationPaths appPaths, + ILogger logger, + WebHostBuilderContext builderContext, + KestrelServerOptions options) + { + bool flagged = false; + foreach (var netAdd in addresses) + { + var address = netAdd.Address; + logger.LogInformation("Kestrel is listening on {Address}", address.Equals(IPAddress.IPv6Any) ? "all interfaces" : address); + options.Listen(netAdd.Address, httpPort); + if (httpsPort.HasValue) + { + if (builderContext.HostingEnvironment.IsDevelopment()) { - var address = netAdd.Address; - logger.LogInformation("Kestrel is listening on {Address}", address.Equals(IPAddress.IPv6Any) ? "all interfaces" : address); - options.Listen(netAdd.Address, appHost.HttpPort); - if (appHost.ListenWithHttps) + try { options.Listen( address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); + httpsPort.Value, + listenOptions => listenOptions.UseHttps()); } - else if (builderContext.HostingEnvironment.IsDevelopment()) + catch (InvalidOperationException) { - try + if (!flagged) { - options.Listen( - address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps()); - } - catch (InvalidOperationException) - { - if (!flagged) - { - logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted"); - flagged = true; - } + logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted"); + flagged = true; } } } - - // Bind to unix socket (only on unix systems) - if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) + else { - var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); - - // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 - if (File.Exists(socketPath)) + if (certificate is null) { - File.Delete(socketPath); + throw new InvalidOperationException("Cannot run jellyfin with https without setting a valid certificate."); } - options.ListenUnixSocket(socketPath); - logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); + options.Listen( + address, + httpsPort.Value, + listenOptions => listenOptions.UseHttps(certificate)); } - }) - .UseStartup(context => new Startup(appHost, context.Configuration)); + } + } + + // Bind to unix socket (only on unix systems) + if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) + { + var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); + + // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 + if (File.Exists(socketPath)) + { + File.Delete(socketPath); + } + + options.ListenUnixSocket(socketPath); + logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); + } } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index e661d0d4a..55a4a0087 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -46,7 +46,7 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); - private static SetupServer _setupServer = new(); + private static SetupServer? _setupServer; private static CoreAppHost? _appHost; private static IHost? _jellyfinHost = null; private static long _startTimestamp; @@ -75,7 +75,6 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -88,7 +87,8 @@ namespace Jellyfin.Server // Create an instance of the application configuration to use for application startup IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); - + _setupServer = new SetupServer(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost, _loggerFactory, startupConfig); + await _setupServer.RunAsync().ConfigureAwait(false); StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); @@ -130,10 +130,12 @@ namespace Jellyfin.Server if (_restartOnShutdown) { _startTimestamp = Stopwatch.GetTimestamp(); - _setupServer = new SetupServer(); - await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.StopAsync().ConfigureAwait(false); + await _setupServer.RunAsync().ConfigureAwait(false); } } while (_restartOnShutdown); + + _setupServer.Dispose(); } private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) @@ -170,9 +172,7 @@ namespace Jellyfin.Server try { - await _setupServer.StopAsync().ConfigureAwait(false); - _setupServer.Dispose(); - _setupServer = null!; + await _setupServer!.StopAsync().ConfigureAwait(false); await _jellyfinHost.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 9e2cf5bc8..3d4810bd7 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -4,6 +4,9 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Emby.Server.Implementations.Configuration; +using Emby.Server.Implementations.Serialization; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -11,9 +14,12 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; namespace Jellyfin.Server.ServerSetupApp; @@ -22,20 +28,45 @@ namespace Jellyfin.Server.ServerSetupApp; /// </summary> public sealed class SetupServer : IDisposable { + private readonly Func<INetworkManager?> _networkManagerFactory; + private readonly IApplicationPaths _applicationPaths; + private readonly Func<IServerApplicationHost?> _serverFactory; + private readonly ILoggerFactory _loggerFactory; + private readonly IConfiguration _startupConfiguration; + private readonly ServerConfigurationManager _configurationManager; private IHost? _startupServer; private bool _disposed; /// <summary> - /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup. + /// Initializes a new instance of the <see cref="SetupServer"/> class. /// </summary> /// <param name="networkManagerFactory">The networkmanager.</param> /// <param name="applicationPaths">The application paths.</param> - /// <param name="serverApplicationHost">The servers application host.</param> - /// <returns>A Task.</returns> - public async Task RunAsync( + /// <param name="serverApplicationHostFactory">The servers application host.</param> + /// <param name="loggerFactory">The logger factory.</param> + /// <param name="startupConfiguration">The startup configuration.</param> + public SetupServer( Func<INetworkManager?> networkManagerFactory, IApplicationPaths applicationPaths, - Func<IServerApplicationHost?> serverApplicationHost) + Func<IServerApplicationHost?> serverApplicationHostFactory, + ILoggerFactory loggerFactory, + IConfiguration startupConfiguration) + { + _networkManagerFactory = networkManagerFactory; + _applicationPaths = applicationPaths; + _serverFactory = serverApplicationHostFactory; + _loggerFactory = loggerFactory; + _startupConfiguration = startupConfiguration; + var xmlSerializer = new MyXmlSerializer(); + _configurationManager = new ServerConfigurationManager(_applicationPaths, loggerFactory, xmlSerializer); + _configurationManager.RegisterConfiguration<NetworkConfigurationFactory>(); + } + + /// <summary> + /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup. + /// </summary> + /// <returns>A Task.</returns> + public async Task RunAsync() { ThrowIfDisposed(); _startupServer = Host.CreateDefaultBuilder() @@ -48,7 +79,23 @@ public sealed class SetupServer : IDisposable .ConfigureWebHostDefaults(webHostBuilder => { webHostBuilder - .UseKestrel() + .UseKestrel((builderContext, options) => + { + var config = _configurationManager.GetNetworkConfiguration()!; + var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger<SetupServer>(), config.EnableIPv4, config.EnableIPv6); + knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6); + var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6); + Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer( + bindInterfaces, + config.InternalHttpPort, + null, + null, + _startupConfiguration, + _applicationPaths, + _loggerFactory.CreateLogger<SetupServer>(), + builderContext, + options); + }) .Configure(app => { app.UseHealthChecks("/health"); @@ -57,14 +104,14 @@ public sealed class SetupServer : IDisposable { loggerRoute.Run(async context => { - var networkManager = networkManagerFactory(); + var networkManager = _networkManagerFactory(); if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; } - var logFilePath = new DirectoryInfo(applicationPaths.LogDirectoryPath) + var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath) .EnumerateFiles() .OrderBy(f => f.CreationTimeUtc) .FirstOrDefault() @@ -80,20 +127,20 @@ public sealed class SetupServer : IDisposable { systemRoute.Run(async context => { - var jfApplicationHost = serverApplicationHost(); + var jfApplicationHost = _serverFactory(); var retryCounter = 0; while (jfApplicationHost is null && retryCounter < 5) { await Task.Delay(500).ConfigureAwait(false); - jfApplicationHost = serverApplicationHost(); + jfApplicationHost = _serverFactory(); retryCounter++; } if (jfApplicationHost is null) { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + context.Response.Headers.RetryAfter = new StringValues("5"); return; } @@ -114,9 +161,10 @@ public sealed class SetupServer : IDisposable app.Run((context) => { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + context.Response.Headers.RetryAfter = new StringValues("5"); + context.Response.Headers.ContentType = new StringValues("text/html"); context.Response.WriteAsync("<p>Jellyfin Server still starting. Please wait.</p>"); - var networkManager = networkManagerFactory(); + var networkManager = _networkManagerFactory(); if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) { context.Response.WriteAsync("<p>You can download the current logfiles <a href='/startup/logger'>here</a>.</p>"); |
