Selaa lähdekoodia

Merge pull request #4043 from cvium/remove_shit_and_shit_adjacent_shit

Split HttpListenerHost into middlewares
Anthony Lavado 4 vuotta sitten
vanhempi
sitoutus
a2d6ea2eed

+ 12 - 11
Emby.Server.Implementations/ApplicationHost.cs

@@ -96,12 +96,12 @@ using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
 using MediaBrowser.XbmcMetadata.Providers;
-using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Prometheus.DotNetRuntime;
 using Prometheus.DotNetRuntime;
 using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
+using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
 
 
 namespace Emby.Server.Implementations
 namespace Emby.Server.Implementations
 {
 {
@@ -122,14 +122,18 @@ namespace Emby.Server.Implementations
 
 
         private IMediaEncoder _mediaEncoder;
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
         private ISessionManager _sessionManager;
-        private IHttpServer _httpServer;
+        private IWebSocketManager _webSocketManager;
         private IHttpClient _httpClient;
         private IHttpClient _httpClient;
 
 
+        private string[] _urlPrefixes;
+
         /// <summary>
         /// <summary>
         /// Gets a value indicating whether this instance can self restart.
         /// Gets a value indicating whether this instance can self restart.
         /// </summary>
         /// </summary>
         public bool CanSelfRestart => _startupOptions.RestartPath != null;
         public bool CanSelfRestart => _startupOptions.RestartPath != null;
 
 
+        public bool CoreStartupHasCompleted { get; private set; }
+
         public virtual bool CanLaunchWebBrowser
         public virtual bool CanLaunchWebBrowser
         {
         {
             get
             get
@@ -444,8 +448,7 @@ namespace Emby.Server.Implementations
             Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
             Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
 
 
             Logger.LogInformation("Core startup complete");
             Logger.LogInformation("Core startup complete");
-            _httpServer.GlobalResponse = null;
-
+            CoreStartupHasCompleted = true;
             stopWatch.Restart();
             stopWatch.Restart();
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
             Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
             Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
@@ -500,9 +503,6 @@ namespace Emby.Server.Implementations
             RegisterServices();
             RegisterServices();
         }
         }
 
 
-        public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
-            => _httpServer.RequestHandler(context);
-
         /// <summary>
         /// <summary>
         /// Registers services/resources with the service collection that will be available via DI.
         /// Registers services/resources with the service collection that will be available via DI.
         /// </summary>
         /// </summary>
@@ -577,7 +577,7 @@ namespace Emby.Server.Implementations
 
 
             ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
             ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
 
-            ServiceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
+            ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
 
 
             ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
             ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
 
 
@@ -650,7 +650,7 @@ namespace Emby.Server.Implementations
 
 
             _mediaEncoder = Resolve<IMediaEncoder>();
             _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
             _sessionManager = Resolve<ISessionManager>();
-            _httpServer = Resolve<IHttpServer>();
+            _webSocketManager = Resolve<IWebSocketManager>();
             _httpClient = Resolve<IHttpClient>();
             _httpClient = Resolve<IHttpClient>();
 
 
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@@ -771,7 +771,8 @@ namespace Emby.Server.Implementations
                         .Where(i => i != null)
                         .Where(i => i != null)
                         .ToArray();
                         .ToArray();
 
 
-            _httpServer.Init(GetExports<IWebSocketListener>(), GetUrlPrefixes());
+            _urlPrefixes = GetUrlPrefixes().ToArray();
+            _webSocketManager.Init(GetExports<IWebSocketListener>());
 
 
             Resolve<ILibraryManager>().AddParts(
             Resolve<ILibraryManager>().AddParts(
                 GetExports<IResolverIgnoreRule>(),
                 GetExports<IResolverIgnoreRule>(),
@@ -937,7 +938,7 @@ namespace Emby.Server.Implementations
                 }
                 }
             }
             }
 
 
-            if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
+            if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
             {
             {
                 requiresRestart = true;
                 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>
         public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
         {
         {
             { HostWebClientKey, bool.TrueString },
             { HostWebClientKey, bool.TrueString },
-            { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
+            { DefaultRedirectKey, "web/index.html" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { PlaylistsAllowDuplicatesKey, bool.TrueString },
             { PlaylistsAllowDuplicatesKey, bool.TrueString },

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

@@ -1,559 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Net.Sockets;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Data.Events;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Globalization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.AspNetCore.WebUtilities;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Hosting;
-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 IServerApplicationHost _appHost;
-        private readonly string _defaultRedirectPath;
-        private readonly string _baseUrlPrefix;
-
-        private readonly IHostEnvironment _hostEnvironment;
-
-        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
-        private bool _disposed = false;
-
-        public HttpListenerHost(
-            IServerApplicationHost applicationHost,
-            ILogger<HttpListenerHost> logger,
-            IServerConfigurationManager config,
-            IConfiguration configuration,
-            INetworkManager networkManager,
-            ILocalizationManager localizationManager,
-            IHostEnvironment hostEnvironment,
-            ILoggerFactory loggerFactory)
-        {
-            _appHost = applicationHost;
-            _logger = logger;
-            _config = config;
-            _defaultRedirectPath = configuration[DefaultRedirectKey];
-            _baseUrlPrefix = _config.Configuration.BaseUrl;
-            _networkManager = networkManager;
-            _hostEnvironment = hostEnvironment;
-            _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 Exception GetActualException(Exception ex)
-        {
-            if (ex is AggregateException agg)
-            {
-                var inner = agg.InnerException;
-                if (inner != null)
-                {
-                    return GetActualException(inner);
-                }
-                else
-                {
-                    var inners = agg.InnerExceptions;
-                    if (inners.Count > 0)
-                    {
-                        return GetActualException(inners[0]);
-                    }
-                }
-            }
-
-            return ex;
-        }
-
-        private int GetStatusCode(Exception ex)
-        {
-            switch (ex)
-            {
-                case ArgumentException _: return 400;
-                case AuthenticationException _: return 401;
-                case SecurityException _: return 403;
-                case DirectoryNotFoundException _:
-                case FileNotFoundException _:
-                case ResourceNotFoundException _: return 404;
-                case MethodNotAllowedException _: return 405;
-                default: return 500;
-            }
-        }
-
-        private async Task ErrorHandler(Exception ex, HttpContext httpContext, int statusCode, string urlToLog, bool ignoreStackTrace)
-        {
-            if (ignoreStackTrace)
-            {
-                _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
-            }
-            else
-            {
-                _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
-            }
-
-            var httpRes = httpContext.Response;
-
-            if (httpRes.HasStarted)
-            {
-                return;
-            }
-
-            httpRes.StatusCode = statusCode;
-
-            var errContent = _hostEnvironment.IsDevelopment()
-                    ? (NormalizeExceptionMessage(ex) ?? string.Empty)
-                    : "Error processing request.";
-            httpRes.ContentType = "text/plain";
-            httpRes.ContentLength = errContent.Length;
-            await httpRes.WriteAsync(errContent).ConfigureAwait(false);
-        }
-
-        private string NormalizeExceptionMessage(Exception ex)
-        {
-            // Do not expose the exception message for AuthenticationException
-            if (ex is AuthenticationException)
-            {
-                return null;
-            }
-
-            // Strip any information we don't want to reveal
-            return ex.Message
-                ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase)
-                .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
-        }
-
-        public static string RemoveQueryStringByKey(string url, string key)
-        {
-            var uri = new Uri(url);
-
-            // this gets all the query string key value pairs as a collection
-            var newQueryString = QueryHelpers.ParseQuery(uri.Query);
-
-            var originalCount = newQueryString.Count;
-
-            if (originalCount == 0)
-            {
-                return url;
-            }
-
-            // this removes the key if exists
-            newQueryString.Remove(key);
-
-            if (originalCount == newQueryString.Count)
-            {
-                return url;
-            }
-
-            // this gets the page path from root without QueryString
-            string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
-
-            return newQueryString.Count > 0
-                ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
-                : pagePathWithoutQueryString;
-        }
-
-        private static string GetUrlToLog(string url)
-        {
-            url = RemoveQueryStringByKey(url, "api_key");
-
-            return url;
-        }
-
-        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;
-        }
-
-        /// <summary>
-        /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
-        /// </summary>
-        /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
-        private bool ValidateSsl(string remoteIp, string urlString)
-        {
-            if (_config.Configuration.RequireHttps
-                && _appHost.ListenWithHttps
-                && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
-            {
-                // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
-                if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
-                    || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
-                {
-                    return true;
-                }
-
-                if (!_networkManager.IsInLocalNetwork(remoteIp))
-                {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-
-        /// <inheritdoc />
-        public Task RequestHandler(HttpContext context)
-        {
-            if (context.WebSockets.IsWebSocketRequest)
-            {
-                return WebSocketRequestHandler(context);
-            }
-
-            return RequestHandler(context, context.RequestAborted);
-        }
-
-        /// <summary>
-        /// Overridable method that can be used to implement a custom handler.
-        /// </summary>
-        private async Task RequestHandler(HttpContext httpContext, CancellationToken cancellationToken)
-        {
-            var stopWatch = new Stopwatch();
-            stopWatch.Start();
-            var httpRes = httpContext.Response;
-            var host = httpContext.Request.Host.ToString();
-            var localPath = httpContext.Request.Path.ToString();
-            var urlString = httpContext.Request.GetDisplayUrl();
-            string urlToLog = GetUrlToLog(urlString);
-            string remoteIp = httpContext.Request.RemoteIp();
-
-            try
-            {
-                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 (!ValidateSsl(httpContext.Request.RemoteIp(), urlString))
-                {
-                    RedirectToSecureUrl(httpRes, urlString);
-                    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;
-                    }
-                }
-
-                throw new FileNotFoundException();
-            }
-            catch (Exception requestEx)
-            {
-                try
-                {
-                    var requestInnerEx = GetActualException(requestEx);
-                    var statusCode = GetStatusCode(requestInnerEx);
-
-                    foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
-                    {
-                        if (!httpRes.Headers.ContainsKey(key))
-                        {
-                            httpRes.Headers.Add(key, value);
-                        }
-                    }
-
-                    bool ignoreStackTrace =
-                        requestInnerEx is SocketException
-                        || requestInnerEx is IOException
-                        || requestInnerEx is OperationCanceledException
-                        || requestInnerEx is SecurityException
-                        || requestInnerEx is AuthenticationException
-                        || requestInnerEx is FileNotFoundException;
-
-                    // Do not handle 500 server exceptions manually when in development mode.
-                    // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
-                    // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
-                    // because it will log the stack trace when it handles the exception.
-                    if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
-                    {
-                        throw;
-                    }
-
-                    await ErrorHandler(requestInnerEx, httpContext, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
-                }
-                catch (Exception handlerException)
-                {
-                    var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException);
-                    _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog);
-
-                    if (_hostEnvironment.IsDevelopment())
-                    {
-                        throw aggregateEx;
-                    }
-                }
-            }
-            finally
-            {
-                if (httpRes.StatusCode >= 500)
-                {
-                    _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
-                }
-
-                stopWatch.Stop();
-                var elapsed = stopWatch.Elapsed;
-                if (elapsed.TotalMilliseconds > 500)
-                {
-                    _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
-                }
-            }
-        }
-
-        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;
-        }
-
-        private void RedirectToSecureUrl(HttpResponse httpRes, string url)
-        {
-            if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
-            {
-                var builder = new UriBuilder(uri)
-                {
-                    Port = _config.Configuration.PublicHttpsPort,
-                    Scheme = "https"
-                };
-                url = builder.Uri.ToString();
-            }
-
-            httpRes.Redirect(url);
-        }
-
-        /// <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 ILogger<SessionWebSocketListener> _logger;
         private readonly ILoggerFactory _loggerFactory;
         private readonly ILoggerFactory _loggerFactory;
 
 
-        private readonly IHttpServer _httpServer;
+        private readonly IWebSocketManager _webSocketManager;
 
 
         /// <summary>
         /// <summary>
         /// The KeepAlive cancellation token.
         /// The KeepAlive cancellation token.
@@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="loggerFactory">The logger factory.</param>
         /// <param name="loggerFactory">The logger factory.</param>
-        /// <param name="httpServer">The HTTP server.</param>
+        /// <param name="webSocketManager">The HTTP server.</param>
         public SessionWebSocketListener(
         public SessionWebSocketListener(
             ILogger<SessionWebSocketListener> logger,
             ILogger<SessionWebSocketListener> logger,
             ISessionManager sessionManager,
             ISessionManager sessionManager,
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
-            IHttpServer httpServer)
+            IWebSocketManager webSocketManager)
         {
         {
             _logger = logger;
             _logger = logger;
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
             _loggerFactory = loggerFactory;
             _loggerFactory = loggerFactory;
-            _httpServer = httpServer;
+            _webSocketManager = webSocketManager;
 
 
-            httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
+            webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected;
         }
         }
 
 
         private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
         private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session
         /// <inheritdoc />
         /// <inheritdoc />
         public void Dispose()
         public void Dispose()
         {
         {
-            _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
+            _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected;
             StopKeepAlive();
             StopKeepAlive();
         }
         }
 
 

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

@@ -1,3 +1,4 @@
+using Jellyfin.Server.Middleware;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Builder;
 
 
@@ -46,5 +47,55 @@ namespace Jellyfin.Server.Extensions
                     c.RoutePrefix = $"{baseUrl}api-docs/redoc";
                     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 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);
+        }
+    }
+}

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

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

@@ -0,0 +1,49 @@
+using System.Net.Mime;
+using System.Threading.Tasks;
+using MediaBrowser.Controller;
+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="serverApplicationHost">The server application host.</param>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <returns>The async task.</returns>
+        public async Task Invoke(
+            HttpContext httpContext,
+            IServerApplicationHost serverApplicationHost,
+            ILocalizationManager localizationManager)
+        {
+            if (serverApplicationHost.CoreStartupHasCompleted)
+            {
+                await _next(httpContext).ConfigureAwait(false);
+                return;
+            }
+
+            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 System.Threading.Tasks;
 using CommandLine;
 using CommandLine;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations;
-using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.Networking;
 using Emby.Server.Implementations.Networking;
 using Jellyfin.Api.Controllers;
 using Jellyfin.Api.Controllers;
@@ -28,6 +27,7 @@ using Microsoft.Extensions.Logging.Abstractions;
 using Serilog;
 using Serilog;
 using Serilog.Extensions.Logging;
 using Serilog.Extensions.Logging;
 using SQLitePCL;
 using SQLitePCL;
+using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 using ILogger = Microsoft.Extensions.Logging.ILogger;
 using ILogger = Microsoft.Extensions.Logging.ILogger;
 
 
 namespace Jellyfin.Server
 namespace Jellyfin.Server
@@ -594,7 +594,7 @@ namespace Jellyfin.Server
             var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
             var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
             if (startupConfig != null && !startupConfig.HostWebClient())
             if (startupConfig != null && !startupConfig.HostWebClient())
             {
             {
-                inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger";
+                inMemoryDefaultConfig[ConfigurationExtensions.DefaultRedirectKey] = "api-docs/swagger";
             }
             }
 
 
             return config
             return config

+ 31 - 14
Jellyfin.Server/Startup.cs

@@ -23,17 +23,19 @@ namespace Jellyfin.Server
     public class Startup
     public class Startup
     {
     {
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly IApplicationHost _applicationHost;
+        private readonly IServerApplicationHost _serverApplicationHost;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="Startup" /> class.
         /// Initializes a new instance of the <see cref="Startup" /> class.
         /// </summary>
         /// </summary>
         /// <param name="serverConfigurationManager">The server configuration manager.</param>
         /// <param name="serverConfigurationManager">The server configuration manager.</param>
-        /// <param name="applicationHost">The application host.</param>
-        public Startup(IServerConfigurationManager serverConfigurationManager, IApplicationHost applicationHost)
+        /// <param name="serverApplicationHost">The server application host.</param>
+        public Startup(
+            IServerConfigurationManager serverConfigurationManager,
+            IServerApplicationHost serverApplicationHost)
         {
         {
             _serverConfigurationManager = serverConfigurationManager;
             _serverConfigurationManager = serverConfigurationManager;
-            _applicationHost = applicationHost;
+            _serverApplicationHost = serverApplicationHost;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -44,7 +46,13 @@ namespace Jellyfin.Server
         {
         {
             services.AddResponseCompression();
             services.AddResponseCompression();
             services.AddHttpContextAccessor();
             services.AddHttpContextAccessor();
-            services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), _applicationHost.GetApiPluginAssemblies());
+            services.AddHttpsRedirection(options =>
+            {
+                options.HttpsPort = _serverApplicationHost.HttpsPort;
+            });
+            services.AddJellyfinApi(
+                _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'),
+                _serverApplicationHost.GetApiPluginAssemblies());
 
 
             services.AddJellyfinApiSwagger();
             services.AddJellyfinApiSwagger();
 
 
@@ -53,7 +61,9 @@ namespace Jellyfin.Server
 
 
             services.AddJellyfinApiAuthorization();
             services.AddJellyfinApiAuthorization();
 
 
-            var productHeader = new ProductInfoHeaderValue(_applicationHost.Name.Replace(' ', '-'), _applicationHost.ApplicationVersionString);
+            var productHeader = new ProductInfoHeaderValue(
+                _serverApplicationHost.Name.Replace(' ', '-'),
+                _serverApplicationHost.ApplicationVersionString);
             services
             services
                 .AddHttpClient(NamedClient.Default, c =>
                 .AddHttpClient(NamedClient.Default, c =>
                 {
                 {
@@ -64,7 +74,7 @@ namespace Jellyfin.Server
             services.AddHttpClient(NamedClient.MusicBrainz, c =>
             services.AddHttpClient(NamedClient.MusicBrainz, c =>
                 {
                 {
                     c.DefaultRequestHeaders.UserAgent.Add(productHeader);
                     c.DefaultRequestHeaders.UserAgent.Add(productHeader);
-                    c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_applicationHost.ApplicationUserAgentAddress})"));
+                    c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})"));
                 })
                 })
                 .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
                 .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
         }
         }
@@ -74,11 +84,9 @@ namespace Jellyfin.Server
         /// </summary>
         /// </summary>
         /// <param name="app">The application builder.</param>
         /// <param name="app">The application builder.</param>
         /// <param name="env">The webhost environment.</param>
         /// <param name="env">The webhost environment.</param>
-        /// <param name="serverApplicationHost">The server application host.</param>
         public void Configure(
         public void Configure(
             IApplicationBuilder app,
             IApplicationBuilder app,
-            IWebHostEnvironment env,
-            IServerApplicationHost serverApplicationHost)
+            IWebHostEnvironment env)
         {
         {
             if (env.IsDevelopment())
             if (env.IsDevelopment())
             {
             {
@@ -93,12 +101,17 @@ namespace Jellyfin.Server
 
 
             app.UseResponseCompression();
             app.UseResponseCompression();
 
 
-            // TODO app.UseMiddleware<WebSocketMiddleware>();
+            app.UseCors(ServerCorsPolicy.DefaultPolicyName);
+
+            if (_serverConfigurationManager.Configuration.RequireHttps
+                && _serverApplicationHost.ListenWithHttps)
+            {
+                app.UseHttpsRedirection();
+            }
 
 
             app.UseAuthentication();
             app.UseAuthentication();
             app.UseJellyfinApiSwagger(_serverConfigurationManager);
             app.UseJellyfinApiSwagger(_serverConfigurationManager);
             app.UseRouting();
             app.UseRouting();
-            app.UseCors(ServerCorsPolicy.DefaultPolicyName);
             app.UseAuthorization();
             app.UseAuthorization();
             if (_serverConfigurationManager.Configuration.EnableMetrics)
             if (_serverConfigurationManager.Configuration.EnableMetrics)
             {
             {
@@ -106,6 +119,12 @@ namespace Jellyfin.Server
                 app.UseHttpMetrics();
                 app.UseHttpMetrics();
             }
             }
 
 
+            app.UseLanFiltering();
+            app.UseIpBasedAccessValidation();
+            app.UseBaseUrlRedirection();
+            app.UseWebSocketHandler();
+            app.UseServerStartupMessage();
+
             app.UseEndpoints(endpoints =>
             app.UseEndpoints(endpoints =>
             {
             {
                 endpoints.MapControllers();
                 endpoints.MapControllers();
@@ -115,8 +134,6 @@ namespace Jellyfin.Server
                 }
                 }
             });
             });
 
 
-            app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
-
             // Add type descriptor for legacy datetime parsing.
             // Add type descriptor for legacy datetime parsing.
             TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
             TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
         }
         }

+ 1 - 1
MediaBrowser.Common/Extensions/HttpContextExtensions.cs

@@ -28,7 +28,7 @@ namespace MediaBrowser.Common.Extensions
         /// <returns>The remote caller IP address.</returns>
         /// <returns>The remote caller IP address.</returns>
         public static string RemoteIp(this HttpRequest request)
         public static string RemoteIp(this HttpRequest request)
         {
         {
-            var cachedRemoteIp = request.HttpContext.Items["RemoteIp"].ToString();
+            var cachedRemoteIp = request.HttpContext.Items["RemoteIp"]?.ToString();
             if (!string.IsNullOrEmpty(cachedRemoteIp))
             if (!string.IsNullOrEmpty(cachedRemoteIp))
             {
             {
                 return cachedRemoteIp;
                 return cachedRemoteIp;

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

@@ -8,6 +8,12 @@ namespace MediaBrowser.Controller.Extensions
     /// </summary>
     /// </summary>
     public static class ConfigurationExtensions
     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>
         /// <summary>
         /// The key for a setting that indicates whether the application should host web client content.
         /// The key for a setting that indicates whether the application should host web client content.
         /// </summary>
         /// </summary>

+ 3 - 2
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -20,6 +20,8 @@ namespace MediaBrowser.Controller
 
 
         IServiceProvider ServiceProvider { get; }
         IServiceProvider ServiceProvider { get; }
 
 
+        bool CoreStartupHasCompleted { get; }
+
         bool CanLaunchWebBrowser { get; }
         bool CanLaunchWebBrowser { get; }
 
 
         /// <summary>
         /// <summary>
@@ -117,8 +119,7 @@ namespace MediaBrowser.Controller
         IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo();
         IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo();
 
 
         string ExpandVirtualPath(string path);
         string ExpandVirtualPath(string path);
-        string ReverseVirtualPath(string path);
 
 
-        Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
+        string ReverseVirtualPath(string path);
     }
     }
 }
 }

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

@@ -1,49 +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"></param>
-        /// <returns></returns>
-        Task RequestHandler(HttpContext context);
-
-        /// <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);
+    }
+}