ソースを参照

Kill HttpListenerHost

Claus Vium 4 年 前
コミット
571d0570f5

+ 9 - 10
Emby.Server.Implementations/ApplicationHost.cs

@@ -96,12 +96,12 @@ using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
-using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Prometheus.DotNetRuntime;
 using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
+using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
 
 namespace Emby.Server.Implementations
 {
@@ -122,9 +122,11 @@ namespace Emby.Server.Implementations
 
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
-        private IHttpServer _httpServer;
+        private IWebSocketManager _webSocketManager;
         private IHttpClient _httpClient;
 
+        private string[] _urlPrefixes;
+
         /// <summary>
         /// Gets a value indicating whether this instance can self restart.
         /// </summary>
@@ -444,7 +446,6 @@ namespace Emby.Server.Implementations
             Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
 
             Logger.LogInformation("Core startup complete");
-            _httpServer.GlobalResponse = null;
 
             stopWatch.Restart();
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@@ -500,9 +501,6 @@ namespace Emby.Server.Implementations
             RegisterServices();
         }
 
-        public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
-            => _httpServer.RequestHandler(context, next);
-
         /// <summary>
         /// Registers services/resources with the service collection that will be available via DI.
         /// </summary>
@@ -577,7 +575,7 @@ namespace Emby.Server.Implementations
 
             ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
-            ServiceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
+            ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
 
             ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
 
@@ -650,7 +648,7 @@ namespace Emby.Server.Implementations
 
             _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
-            _httpServer = Resolve<IHttpServer>();
+            _webSocketManager = Resolve<IWebSocketManager>();
             _httpClient = Resolve<IHttpClient>();
 
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@@ -771,7 +769,8 @@ namespace Emby.Server.Implementations
                         .Where(i => i != null)
                         .ToArray();
 
-            _httpServer.Init(GetExports<IWebSocketListener>(), GetUrlPrefixes());
+            _urlPrefixes = GetUrlPrefixes().ToArray();
+            _webSocketManager.Init(GetExports<IWebSocketListener>());
 
             Resolve<ILibraryManager>().AddParts(
                 GetExports<IResolverIgnoreRule>(),
@@ -937,7 +936,7 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
+            if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
             {
                 requiresRestart = true;
             }

+ 1 - 1
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations
         public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
         {
             { HostWebClientKey, bool.TrueString },
-            { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
+            { DefaultRedirectKey, "web/index.html" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { PlaylistsAllowDuplicatesKey, bool.TrueString },

+ 0 - 315
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -1,315 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Threading.Tasks;
-using Jellyfin.Data.Events;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Globalization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    public class HttpListenerHost : IHttpServer
-    {
-        /// <summary>
-        /// The key for a setting that specifies the default redirect path
-        /// to use for requests where the URL base prefix is invalid or missing.
-        /// </summary>
-        public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
-
-        private readonly ILogger<HttpListenerHost> _logger;
-        private readonly ILoggerFactory _loggerFactory;
-        private readonly IServerConfigurationManager _config;
-        private readonly INetworkManager _networkManager;
-        private readonly string _defaultRedirectPath;
-        private readonly string _baseUrlPrefix;
-
-        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
-        private bool _disposed = false;
-
-        public HttpListenerHost(
-            ILogger<HttpListenerHost> logger,
-            IServerConfigurationManager config,
-            IConfiguration configuration,
-            INetworkManager networkManager,
-            ILocalizationManager localizationManager,
-            ILoggerFactory loggerFactory)
-        {
-            _logger = logger;
-            _config = config;
-            _defaultRedirectPath = configuration[DefaultRedirectKey];
-            _baseUrlPrefix = _config.Configuration.BaseUrl;
-            _networkManager = networkManager;
-            _loggerFactory = loggerFactory;
-
-            Instance = this;
-            GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
-        }
-
-        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
-
-        public static HttpListenerHost Instance { get; protected set; }
-
-        public string[] UrlPrefixes { get; private set; }
-
-        public string GlobalResponse { get; set; }
-
-        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();
-        }
-
-        private bool ValidateHost(string host)
-        {
-            var hosts = _config
-                .Configuration
-                .LocalNetworkAddresses
-                .Select(NormalizeConfiguredLocalAddress)
-                .ToList();
-
-            if (hosts.Count == 0)
-            {
-                return true;
-            }
-
-            host ??= string.Empty;
-
-            if (_networkManager.IsInPrivateAddressSpace(host))
-            {
-                hosts.Add("localhost");
-                hosts.Add("127.0.0.1");
-
-                return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1);
-            }
-
-            return true;
-        }
-
-        private bool ValidateRequest(string remoteIp, bool isLocal)
-        {
-            if (isLocal)
-            {
-                return true;
-            }
-
-            if (_config.Configuration.EnableRemoteAccess)
-            {
-                var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-
-                if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp))
-                {
-                    if (_config.Configuration.IsRemoteIPFilterBlacklist)
-                    {
-                        return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter);
-                    }
-                    else
-                    {
-                        return _networkManager.IsAddressInSubnets(remoteIp, addressFilter);
-                    }
-                }
-            }
-            else
-            {
-                if (!_networkManager.IsInLocalNetwork(remoteIp))
-                {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-
-        /// <inheritdoc />
-        public Task RequestHandler(HttpContext context, Func<Task> next)
-        {
-            if (context.WebSockets.IsWebSocketRequest)
-            {
-                return WebSocketRequestHandler(context);
-            }
-
-            return HttpRequestHandler(context, next);
-        }
-
-        /// <summary>
-        /// Overridable method that can be used to implement a custom handler.
-        /// </summary>
-        private async Task HttpRequestHandler(HttpContext httpContext, Func<Task> next)
-        {
-            var cancellationToken = httpContext.RequestAborted;
-            var httpRes = httpContext.Response;
-            var host = httpContext.Request.Host.ToString();
-            var localPath = httpContext.Request.Path.ToString();
-            string remoteIp = httpContext.Request.RemoteIp();
-
-            if (_disposed)
-            {
-                httpRes.StatusCode = 503;
-                httpRes.ContentType = "text/plain";
-                await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false);
-                return;
-            }
-
-            if (!ValidateHost(host))
-            {
-                httpRes.StatusCode = 400;
-                httpRes.ContentType = "text/plain";
-                await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false);
-                return;
-            }
-
-            if (!ValidateRequest(remoteIp, httpContext.Request.IsLocal()))
-            {
-                httpRes.StatusCode = 403;
-                httpRes.ContentType = "text/plain";
-                await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
-                return;
-            }
-
-            if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
-            {
-                httpRes.StatusCode = 200;
-                foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
-                {
-                    httpRes.Headers.Add(key, value);
-                }
-
-                httpRes.ContentType = "text/plain";
-                await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
-                return;
-            }
-
-            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 a URL at {0}", localPath);
-                httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath);
-                return;
-            }
-
-            if (!string.IsNullOrEmpty(GlobalResponse))
-            {
-                // We don't want the address pings in ApplicationHost to fail
-                if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
-                {
-                    httpRes.StatusCode = 503;
-                    httpRes.ContentType = "text/html";
-                    await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false);
-                    return;
-                }
-            }
-
-            await next().ConfigureAwait(false);
-        }
-
-        private async Task WebSocketRequestHandler(HttpContext context)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            try
-            {
-                _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
-
-                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
-
-                using var connection = new WebSocketConnection(
-                    _loggerFactory.CreateLogger<WebSocketConnection>(),
-                    webSocket,
-                    context.Connection.RemoteIpAddress,
-                    context.Request.Query)
-                {
-                    OnReceive = ProcessWebSocketMessageReceived
-                };
-
-                WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
-
-                await connection.ProcessAsync().ConfigureAwait(false);
-                _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
-            }
-            catch (Exception ex) // Otherwise ASP.Net will ignore the exception
-            {
-                _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
-                if (!context.Response.HasStarted)
-                {
-                    context.Response.StatusCode = 500;
-                }
-            }
-        }
-
-        /// <inheritdoc />
-        public IDictionary<string, string> GetDefaultCorsHeaders(HttpContext httpContext)
-        {
-            var origin = httpContext.Request.Headers["Origin"];
-            if (origin == StringValues.Empty)
-            {
-                origin = httpContext.Request.Headers["Host"];
-                if (origin == StringValues.Empty)
-                {
-                    origin = "*";
-                }
-            }
-
-            var headers = new Dictionary<string, string>();
-            headers.Add("Access-Control-Allow-Origin", origin);
-            headers.Add("Access-Control-Allow-Credentials", "true");
-            headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
-            headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
-            return headers;
-        }
-
-        /// <summary>
-        /// Adds the rest handlers.
-        /// </summary>
-        /// <param name="listeners">The web socket listeners.</param>
-        /// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param>
-        public void Init(IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes)
-        {
-            _webSocketListeners = listeners.ToArray();
-            UrlPrefixes = urlPrefixes.ToArray();
-        }
-
-        /// <summary>
-        /// Processes the web socket message received.
-        /// </summary>
-        /// <param name="result">The result.</param>
-        private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
-        {
-            if (_disposed)
-            {
-                return Task.CompletedTask;
-            }
-
-            IEnumerable<Task> GetTasks()
-            {
-                foreach (var x in _webSocketListeners)
-                {
-                    yield return x.ProcessMessageAsync(result);
-                }
-            }
-
-            return Task.WhenAll(GetTasks());
-        }
-    }
-}

+ 102 - 0
Emby.Server.Implementations/HttpServer/WebSocketManager.cs

@@ -0,0 +1,102 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.HttpServer
+{
+    public class WebSocketManager : IWebSocketManager
+    {
+        private readonly ILogger<WebSocketManager> _logger;
+        private readonly ILoggerFactory _loggerFactory;
+
+        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
+        private bool _disposed = false;
+
+        public WebSocketManager(
+            ILogger<WebSocketManager> logger,
+            ILoggerFactory loggerFactory)
+        {
+            _logger = logger;
+            _loggerFactory = loggerFactory;
+        }
+
+        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
+
+        /// <inheritdoc />
+        public async Task WebSocketRequestHandler(HttpContext context)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            try
+            {
+                _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
+
+                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
+
+                using var connection = new WebSocketConnection(
+                    _loggerFactory.CreateLogger<WebSocketConnection>(),
+                    webSocket,
+                    context.Connection.RemoteIpAddress,
+                    context.Request.Query)
+                {
+                    OnReceive = ProcessWebSocketMessageReceived
+                };
+
+                WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
+
+                await connection.ProcessAsync().ConfigureAwait(false);
+                _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+            }
+            catch (Exception ex) // Otherwise ASP.Net will ignore the exception
+            {
+                _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
+                if (!context.Response.HasStarted)
+                {
+                    context.Response.StatusCode = 500;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Adds the rest handlers.
+        /// </summary>
+        /// <param name="listeners">The web socket listeners.</param>
+        public void Init(IEnumerable<IWebSocketListener> listeners)
+        {
+            _webSocketListeners = listeners.ToArray();
+        }
+
+        /// <summary>
+        /// Processes the web socket message received.
+        /// </summary>
+        /// <param name="result">The result.</param>
+        private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
+        {
+            if (_disposed)
+            {
+                return Task.CompletedTask;
+            }
+
+            IEnumerable<Task> GetTasks()
+            {
+                foreach (var x in _webSocketListeners)
+                {
+                    yield return x.ProcessMessageAsync(result);
+                }
+            }
+
+            return Task.WhenAll(GetTasks());
+        }
+    }
+}

+ 6 - 6
Emby.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session
         private readonly ILogger<SessionWebSocketListener> _logger;
         private readonly ILoggerFactory _loggerFactory;
 
-        private readonly IHttpServer _httpServer;
+        private readonly IWebSocketManager _webSocketManager;
 
         /// <summary>
         /// The KeepAlive cancellation token.
@@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session
         /// <param name="logger">The logger.</param>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="loggerFactory">The logger factory.</param>
-        /// <param name="httpServer">The HTTP server.</param>
+        /// <param name="webSocketManager">The HTTP server.</param>
         public SessionWebSocketListener(
             ILogger<SessionWebSocketListener> logger,
             ISessionManager sessionManager,
             ILoggerFactory loggerFactory,
-            IHttpServer httpServer)
+            IWebSocketManager webSocketManager)
         {
             _logger = logger;
             _sessionManager = sessionManager;
             _loggerFactory = loggerFactory;
-            _httpServer = httpServer;
+            _webSocketManager = webSocketManager;
 
-            httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
+            webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected;
         }
 
         private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session
         /// <inheritdoc />
         public void Dispose()
         {
-            _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
+            _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected;
             StopKeepAlive();
         }
 

+ 61 - 0
Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs

@@ -1,3 +1,4 @@
+using Jellyfin.Server.Middleware;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Builder;
 
@@ -46,5 +47,65 @@ 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 CORS OPTIONS request handling to the application pipeline.
+        /// </summary>
+        /// <param name="appBuilder">The application builder.</param>
+        /// <returns>The updated application builder.</returns>
+        public static IApplicationBuilder UseCorsOptionsResponse(this IApplicationBuilder appBuilder)
+        {
+            return appBuilder.UseMiddleware<CorsOptionsResponseMiddleware>();
+        }
+
+        /// <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>();
+        }
     }
 }

+ 62 - 0
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);
+        }
+    }
+}

+ 69 - 0
Jellyfin.Server/Middleware/CorsOptionsResponseMiddleware.cs

@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+
+namespace Jellyfin.Server.Middleware
+{
+    /// <summary>
+    /// Middleware for handling OPTIONS requests.
+    /// </summary>
+    public class CorsOptionsResponseMiddleware
+    {
+        private readonly RequestDelegate _next;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CorsOptionsResponseMiddleware"/> class.
+        /// </summary>
+        /// <param name="next">The next delegate in the pipeline.</param>
+        public CorsOptionsResponseMiddleware(RequestDelegate next)
+        {
+            _next = next;
+        }
+
+        /// <summary>
+        /// Executes the middleware action.
+        /// </summary>
+        /// <param name="httpContext">The current HTTP context.</param>
+        /// <returns>The async task.</returns>
+        public async Task Invoke(HttpContext httpContext)
+        {
+            if (string.Equals(httpContext.Request.Method, HttpMethods.Options, StringComparison.OrdinalIgnoreCase))
+            {
+                httpContext.Response.StatusCode = 200;
+                foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
+                {
+                    httpContext.Response.Headers.Add(key, value);
+                }
+
+                httpContext.Response.ContentType = MediaTypeNames.Text.Plain;
+                await httpContext.Response.WriteAsync(string.Empty, httpContext.RequestAborted).ConfigureAwait(false);
+                return;
+            }
+
+            await _next(httpContext).ConfigureAwait(false);
+        }
+
+        private static IDictionary<string, string> GetDefaultCorsHeaders(HttpContext httpContext)
+        {
+            var origin = httpContext.Request.Headers["Origin"];
+            if (origin == StringValues.Empty)
+            {
+                origin = httpContext.Request.Headers["Host"];
+                if (origin == StringValues.Empty)
+                {
+                    origin = "*";
+                }
+            }
+
+            var headers = new Dictionary<string, string>();
+            headers.Add("Access-Control-Allow-Origin", origin);
+            headers.Add("Access-Control-Allow-Credentials", "true");
+            headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
+            headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
+            return headers;
+        }
+    }
+}

+ 76 - 0
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);
+        }
+    }
+}

+ 76 - 0
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();
+        }
+    }
+}

+ 38 - 0
Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs

@@ -0,0 +1,38 @@
+using System.Net.Mime;
+using System.Threading.Tasks;
+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="localizationManager">The localization manager.</param>
+        /// <returns>The async task.</returns>
+        public async Task Invoke(HttpContext httpContext, ILocalizationManager localizationManager)
+        {
+            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);
+        }
+    }
+}

+ 40 - 0
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);
+        }
+    }
+}

+ 2 - 2
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
@@ -594,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

+ 8 - 4
Jellyfin.Server/Startup.cs

@@ -84,11 +84,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())
             {
@@ -120,7 +118,11 @@ namespace Jellyfin.Server
                 app.UseHttpMetrics();
             }
 
-            app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
+            app.UseLanFiltering();
+            app.UseIpBasedAccessValidation();
+            app.UseCorsOptionsResponse();
+            app.UseBaseUrlRedirection();
+            app.UseWebSocketHandler();
 
             app.UseEndpoints(endpoints =>
             {
@@ -131,6 +133,8 @@ namespace Jellyfin.Server
                 }
             });
 
+            app.UseServerStartupMessage();
+
             // Add type descriptor for legacy datetime parsing.
             TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
         }

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

@@ -8,6 +8,12 @@ namespace MediaBrowser.Controller.Extensions
     /// </summary>
     public static class ConfigurationExtensions
     {
+        /// <summary>
+        /// The key for a setting that specifies the default redirect path
+        /// to use for requests where the URL base prefix is invalid or missing..
+        /// </summary>
+        public const string DefaultRedirectKey = "DefaultRedirectPath";
+
         /// <summary>
         /// The key for a setting that indicates whether the application should host web client content.
         /// </summary>

+ 1 - 2
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -117,8 +117,7 @@ namespace MediaBrowser.Controller
         IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo();
 
         string ExpandVirtualPath(string path);
-        string ReverseVirtualPath(string path);
 
-        Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
+        string ReverseVirtualPath(string path);
     }
 }

+ 0 - 50
MediaBrowser.Controller/Net/IHttpServer.cs

@@ -1,50 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Jellyfin.Data.Events;
-using Microsoft.AspNetCore.Http;
-
-namespace MediaBrowser.Controller.Net
-{
-    /// <summary>
-    /// Interface IHttpServer.
-    /// </summary>
-    public interface IHttpServer
-    {
-        /// <summary>
-        /// Gets the URL prefix.
-        /// </summary>
-        /// <value>The URL prefix.</value>
-        string[] UrlPrefixes { get; }
-
-        /// <summary>
-        /// Occurs when [web socket connected].
-        /// </summary>
-        event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
-
-        /// <summary>
-        /// Inits this instance.
-        /// </summary>
-        void Init(IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes);
-
-        /// <summary>
-        /// If set, all requests will respond with this message.
-        /// </summary>
-        string GlobalResponse { get; set; }
-
-        /// <summary>
-        /// The HTTP request handler.
-        /// </summary>
-        /// <param name="context">The current HTTP context.</param>
-        /// <param name="next">The next middleware in the ASP.NET pipeline.</param>
-        /// <returns>The task.</returns>
-        Task RequestHandler(HttpContext context, Func<Task> next);
-
-        /// <summary>
-        /// Get the default CORS headers.
-        /// </summary>
-        /// <param name="httpContext">The HTTP context of the current request.</param>
-        /// <returns>The default CORS headers for the context.</returns>
-        IDictionary<string, string> GetDefaultCorsHeaders(HttpContext httpContext);
-    }
-}

+ 32 - 0
MediaBrowser.Controller/Net/IWebSocketManager.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events;
+using Microsoft.AspNetCore.Http;
+
+namespace MediaBrowser.Controller.Net
+{
+    /// <summary>
+    /// Interface IHttpServer.
+    /// </summary>
+    public interface IWebSocketManager
+    {
+        /// <summary>
+        /// Occurs when [web socket connected].
+        /// </summary>
+        event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
+
+        /// <summary>
+        /// Inits this instance.
+        /// </summary>
+        /// <param name="listeners">The websocket listeners.</param>
+        void Init(IEnumerable<IWebSocketListener> listeners);
+
+        /// <summary>
+        /// The HTTP request handler.
+        /// </summary>
+        /// <param name="context">The current HTTP context.</param>
+        /// <returns>The task.</returns>
+        Task WebSocketRequestHandler(HttpContext context);
+    }
+}