瀏覽代碼

Add initial management interface support.

Erwin de Haan 4 年之前
父節點
當前提交
c5d900b164

+ 14 - 0
Jellyfin.Api/Attributes/ManagementAttribute.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Jellyfin.Api.Attributes
+{
+    /// <summary>
+    /// Specifies that the marked controller or method is only accessible via the management interface.
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
+    public class ManagementAttribute : Attribute
+    {
+    }
+}

+ 72 - 0
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
+{
+    /// <summary>
+    /// The management controller.
+    /// </summary>
+    [Management]
+    public class ManagementController : BaseJellyfinApiController
+    {
+        private readonly IServerApplicationHost _appHost;
+        private readonly IApplicationPaths _appPaths;
+        private readonly IFileSystem _fileSystem;
+        private readonly INetworkManager _network;
+        private readonly ILogger<ManagementController> _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ManagementController"/> class.
+        /// </summary>
+        /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+        /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
+        /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
+        /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
+        public ManagementController(
+            IServerConfigurationManager serverConfigurationManager,
+            IServerApplicationHost appHost,
+            IFileSystem fileSystem,
+            INetworkManager network,
+            ILogger<ManagementController> logger)
+        {
+            _appPaths = serverConfigurationManager.ApplicationPaths;
+            _appHost = appHost;
+            _fileSystem = fileSystem;
+            _network = network;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Gets information about the server.
+        /// </summary>
+        /// <response code="200">Information retrieved.</response>
+        /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
+        [HttpGet("Test")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<int> GetTest()
+        {
+            return 123456; // secret
+        }
+    }
+}

+ 3 - 0
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;
 

+ 96 - 0
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<ManagementAttribute>(context);
+        }
+
+        private bool HasAttribute<T>(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));
+        }
+    }
+}

+ 34 - 0
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()

+ 39 - 0
MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs

@@ -49,6 +49,21 @@ namespace MediaBrowser.Controller.Extensions
         /// </summary>
         public const string UnixSocketPathKey = "kestrel:socketPath";
 
+        /// <summary>
+        /// The key for a setting that indicates whether the management interface should be enabled.
+        /// </summary>
+        public const string UseManagementInterfaceKey = "management:enabled";
+
+        /// <summary>
+        /// The key for a setting that indicates whether the management interface should listen on localhost.
+        /// </summary>
+        public const string ManagementInterfaceLocalhostPortKey = "management:port";
+
+        /// <summary>
+        /// The key for the management interface socket path.
+        /// </summary>
+        public const string ManagementInterfaceSocketPathKey = "management:socket";
+
         /// <summary>
         /// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
         /// </summary>
@@ -97,5 +112,29 @@ namespace MediaBrowser.Controller.Extensions
         /// <returns>The unix socket path.</returns>
         public static string GetUnixSocketPath(this IConfiguration configuration)
             => configuration[UnixSocketPathKey];
+
+        /// <summary>
+        /// Gets a value indicating whether kestrel should enable the management interface from the <see cref="IConfiguration" />.
+        /// </summary>
+        /// <param name="configuration">The configuration to read the setting from.</param>
+        /// <returns><c>true</c> if kestrel should bind to a unix socket, otherwise <c>false</c>.</returns>
+        public static bool UseManagementInterface(this IConfiguration configuration)
+            => configuration.GetValue<bool>(UseManagementInterfaceKey);
+
+        /// <summary>
+        /// Gets the localhost port for the management interface from the <see cref="IConfiguration" />.
+        /// </summary>
+        /// <param name="configuration">The configuration to read the setting from.</param>
+        /// <returns>The management interface address.</returns>
+        public static int GetManagementInterfaceLocalhostPort(this IConfiguration configuration)
+            => configuration.GetValue<int>(ManagementInterfaceLocalhostPortKey);
+
+        /// <summary>
+        /// Gets the path for the management interface socket from the <see cref="IConfiguration" />.
+        /// </summary>
+        /// <param name="configuration">The configuration to read the setting from.</param>
+        /// <returns>The management interface socket path.</returns>
+        public static string GetManagementInterfaceSocketPath(this IConfiguration configuration)
+            => configuration[ManagementInterfaceSocketPathKey];
     }
 }

+ 1 - 0
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;