From c5d900b1645a7916d110af18056b8a4402f04bc5 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Wed, 21 Oct 2020 17:28:00 +0200 Subject: [PATCH] Add initial management interface support. --- .../Attributes/ManagementAttribute.cs | 14 +++ .../Controllers/ManagementController.cs | 72 ++++++++++++++ .../ApiServiceCollectionExtensions.cs | 3 + .../Filters/ManagementInterfaceFilter.cs | 96 +++++++++++++++++++ Jellyfin.Server/Program.cs | 34 +++++++ .../Extensions/ConfigurationExtensions.cs | 39 ++++++++ .../Configuration/ServerConfiguration.cs | 1 + 7 files changed, 259 insertions(+) create mode 100644 Jellyfin.Api/Attributes/ManagementAttribute.cs create mode 100644 Jellyfin.Api/Controllers/ManagementController.cs create mode 100644 Jellyfin.Server/Filters/ManagementInterfaceFilter.cs diff --git a/Jellyfin.Api/Attributes/ManagementAttribute.cs b/Jellyfin.Api/Attributes/ManagementAttribute.cs new file mode 100644 index 0000000000..acee10ee94 --- /dev/null +++ b/Jellyfin.Api/Attributes/ManagementAttribute.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Jellyfin.Api.Attributes +{ + /// + /// Specifies that the marked controller or method is only accessible via the management interface. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class ManagementAttribute : Attribute + { + } +} diff --git a/Jellyfin.Api/Controllers/ManagementController.cs b/Jellyfin.Api/Controllers/ManagementController.cs new file mode 100644 index 0000000000..44ac1a2578 --- /dev/null +++ b/Jellyfin.Api/Controllers/ManagementController.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Attributes; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The management controller. + /// + [Management] + public class ManagementController : BaseJellyfinApiController + { + private readonly IServerApplicationHost _appHost; + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + private readonly INetworkManager _network; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public ManagementController( + IServerConfigurationManager serverConfigurationManager, + IServerApplicationHost appHost, + IFileSystem fileSystem, + INetworkManager network, + ILogger logger) + { + _appPaths = serverConfigurationManager.ApplicationPaths; + _appHost = appHost; + _fileSystem = fileSystem; + _network = network; + _logger = logger; + } + + /// + /// Gets information about the server. + /// + /// Information retrieved. + /// A with info about the system. + [HttpGet("Test")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetTest() + { + return 123456; // secret + } + } +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index d7b9da5c2f..416ae6d7c2 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -159,6 +159,9 @@ namespace Jellyfin.Server.Extensions }) .AddMvc(opts => { + // Seperate the management routes and the general ones. + opts.Filters.Add(typeof(ManagementInterfaceFilter)); + // Allow requester to change between camelCase and PascalCase opts.RespectBrowserAcceptHeader = true; diff --git a/Jellyfin.Server/Filters/ManagementInterfaceFilter.cs b/Jellyfin.Server/Filters/ManagementInterfaceFilter.cs new file mode 100644 index 0000000000..60f4f3956c --- /dev/null +++ b/Jellyfin.Server/Filters/ManagementInterfaceFilter.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Jellyfin.Api.Attributes; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Configuration; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace Jellyfin.Server.Filters +{ + internal class ManagementInterfaceFilter : IActionFilter + { + private readonly List<(IPAddress Host, int Port)> managementEndpoints; + + public ManagementInterfaceFilter(IConfiguration appConfig) + { + managementEndpoints = new List<(IPAddress Host, int Port)>(); + + if (appConfig.UseManagementInterface()) + { + var socketPath = appConfig.GetManagementInterfaceSocketPath(); + var localhostPort = appConfig.GetManagementInterfaceLocalhostPort(); + bool useDefault = true; + if (!string.IsNullOrEmpty(socketPath)) + { + // TODO make this work, no idea where to get the SocketAddress or something similar + managementEndpoints.Add((IPAddress.Any, 0)); + } + + if (localhostPort > 0) + { + managementEndpoints.Add((IPAddress.Loopback, localhostPort)); + managementEndpoints.Add((IPAddress.IPv6Loopback, localhostPort)); + } + + if (useDefault) + { + managementEndpoints.Add((IPAddress.Loopback, ServerConfiguration.DefaultManagementPort)); + managementEndpoints.Add((IPAddress.IPv6Loopback, ServerConfiguration.DefaultManagementPort)); + } + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } + + public void OnActionExecuting(ActionExecutingContext context) + { + var isManagementRoute = IsManagementRoute(context); + var isManagementListenEntrypoint = IsManagementListenEntrypoint(context); + + if ((isManagementRoute && !isManagementListenEntrypoint) || (!isManagementRoute && isManagementListenEntrypoint)) + { + context.Result = new NotFoundResult(); + } + } + + private bool IsManagementRoute(ActionExecutingContext context) + { + return HasAttribute(context); + } + + private bool HasAttribute(ActionExecutingContext context) + { + var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + if (controllerActionDescriptor != null) + { + // Check if the attribute exists on the action method + if (controllerActionDescriptor.MethodInfo?.GetCustomAttributes(inherit: true)?.Any(a => a.GetType().Equals(typeof(T))) ?? false) + { + return true; + } + + // Check if the attribute exists on the controller + if (controllerActionDescriptor.ControllerTypeInfo?.GetCustomAttributes(typeof(T), true)?.Any() ?? false) + { + return true; + } + } + + return false; + } + + private bool IsManagementListenEntrypoint(ActionExecutingContext context) + { + return managementEndpoints.Contains((context.HttpContext.Connection.LocalIpAddress, context.HttpContext.Connection.LocalPort)); + } + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 5573c04395..4b04281bc7 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -16,6 +16,7 @@ using Emby.Server.Implementations.Networking; using Jellyfin.Api.Controllers; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; @@ -372,6 +373,39 @@ namespace Jellyfin.Server options.ListenUnixSocket(socketPath); _logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); } + + // Enable the Management Interface + if (startupConfig.UseManagementInterface()) + { + var socketPath = startupConfig.GetManagementInterfaceSocketPath(); + var localhostPort = startupConfig.GetManagementInterfaceLocalhostPort(); + bool useDefault = true; + if (!string.IsNullOrEmpty(socketPath)) + { + // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 + if (File.Exists(socketPath)) + { + File.Delete(socketPath); + } + + options.ListenUnixSocket(socketPath); + _logger.LogInformation("Management interface listening to unix socket {SocketPath}", socketPath); + useDefault = false; + } + + if (localhostPort > 0) + { + options.ListenLocalhost(localhostPort); + _logger.LogInformation("Management interface listening to localhost on port {LocalhostPort}", localhostPort); + useDefault = false; + } + + if (useDefault) + { + options.ListenLocalhost(ServerConfiguration.DefaultManagementPort); + _logger.LogInformation("Management interface listening to localhost on default port {DefaultManagementPort}", ServerConfiguration.DefaultManagementPort); + } + } }) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig)) .UseSerilog() diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index f9285c7682..1ea269ea41 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -49,6 +49,21 @@ namespace MediaBrowser.Controller.Extensions /// public const string UnixSocketPathKey = "kestrel:socketPath"; + /// + /// The key for a setting that indicates whether the management interface should be enabled. + /// + public const string UseManagementInterfaceKey = "management:enabled"; + + /// + /// The key for a setting that indicates whether the management interface should listen on localhost. + /// + public const string ManagementInterfaceLocalhostPortKey = "management:port"; + + /// + /// The key for the management interface socket path. + /// + public const string ManagementInterfaceSocketPathKey = "management:socket"; + /// /// Gets a value indicating whether the application should host static web content from the . /// @@ -97,5 +112,29 @@ namespace MediaBrowser.Controller.Extensions /// The unix socket path. public static string GetUnixSocketPath(this IConfiguration configuration) => configuration[UnixSocketPathKey]; + + /// + /// Gets a value indicating whether kestrel should enable the management interface from the . + /// + /// The configuration to read the setting from. + /// true if kestrel should bind to a unix socket, otherwise false. + public static bool UseManagementInterface(this IConfiguration configuration) + => configuration.GetValue(UseManagementInterfaceKey); + + /// + /// Gets the localhost port for the management interface from the . + /// + /// The configuration to read the setting from. + /// The management interface address. + public static int GetManagementInterfaceLocalhostPort(this IConfiguration configuration) + => configuration.GetValue(ManagementInterfaceLocalhostPortKey); + + /// + /// Gets the path for the management interface socket from the . + /// + /// The configuration to read the setting from. + /// The management interface socket path. + public static string GetManagementInterfaceSocketPath(this IConfiguration configuration) + => configuration[ManagementInterfaceSocketPathKey]; } } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 8b78ad842e..3b51a0a360 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Configuration public class ServerConfiguration : BaseApplicationConfiguration { public const int DefaultHttpPort = 8096; + public const int DefaultManagementPort = 12000; public const int DefaultHttpsPort = 8920; private string _baseUrl;