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;