ソースを参照

Move HttpListenerHost middleware up the pipeline

Claus Vium 4 年 前
コミット
5813f8073c

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

@@ -501,7 +501,7 @@ namespace Emby.Server.Implementations
         }
 
         public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
-            => _httpServer.RequestHandler(context);
+            => _httpServer.RequestHandler(context, next);
 
         /// <summary>
         /// Registers services/resources with the service collection that will be available via DI.

+ 52 - 296
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -2,26 +2,17 @@
 
 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;
 
@@ -39,32 +30,25 @@ namespace Emby.Server.Implementations.HttpServer
         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;
@@ -79,122 +63,6 @@ namespace Emby.Server.Implementations.HttpServer
 
         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('/');
@@ -267,187 +135,90 @@ namespace Emby.Server.Implementations.HttpServer
             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)
+        public Task RequestHandler(HttpContext context, Func<Task> next)
         {
             if (context.WebSockets.IsWebSocketRequest)
             {
                 return WebSocketRequestHandler(context);
             }
 
-            return RequestHandler(context, context.RequestAborted);
+            return HttpRequestHandler(context, next);
         }
 
         /// <summary>
         /// Overridable method that can be used to implement a custom handler.
         /// </summary>
-        private async Task RequestHandler(HttpContext httpContext, CancellationToken cancellationToken)
+        private async Task HttpRequestHandler(HttpContext httpContext, Func<Task> next)
         {
-            var stopWatch = new Stopwatch();
-            stopWatch.Start();
+            var cancellationToken = httpContext.RequestAborted;
             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)
             {
-                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;
-                }
+                httpRes.StatusCode = 503;
+                httpRes.ContentType = "text/plain";
+                await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false);
+                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;
-                    }
-                }
+            if (!ValidateHost(host))
+            {
+                httpRes.StatusCode = 400;
+                httpRes.ContentType = "text/plain";
+                await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false);
+                return;
+            }
 
-                throw new FileNotFoundException();
+            if (!ValidateRequest(remoteIp, httpContext.Request.IsLocal()))
+            {
+                httpRes.StatusCode = 403;
+                httpRes.ContentType = "text/plain";
+                await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
+                return;
             }
-            catch (Exception requestEx)
+
+            if (string.Equals(httpContext.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
             {
-                try
+                httpRes.StatusCode = 200;
+                foreach (var (key, value) in GetDefaultCorsHeaders(httpContext))
                 {
-                    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);
+                    httpRes.Headers.Add(key, value);
                 }
-                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;
-                    }
-                }
+                httpRes.ContentType = "text/plain";
+                await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
+                return;
             }
-            finally
+
+            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))
             {
-                if (httpRes.StatusCode >= 500)
-                {
-                    _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
-                }
+                // 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;
+            }
 
-                stopWatch.Stop();
-                var elapsed = stopWatch.Elapsed;
-                if (elapsed.TotalMilliseconds > 500)
+            if (!string.IsNullOrEmpty(GlobalResponse))
+            {
+                // We don't want the address pings in ApplicationHost to fail
+                if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
                 {
-                    _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
+                    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)
@@ -508,21 +279,6 @@ namespace Emby.Server.Implementations.HttpServer
             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>

+ 20 - 10
Jellyfin.Server/Startup.cs

@@ -23,17 +23,19 @@ namespace Jellyfin.Server
     public class Startup
     {
         private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly IApplicationHost _applicationHost;
+        private readonly IServerApplicationHost _serverApplicationHost;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Startup" /> class.
         /// </summary>
         /// <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;
-            _applicationHost = applicationHost;
+            _serverApplicationHost = serverApplicationHost;
         }
 
         /// <summary>
@@ -44,7 +46,9 @@ namespace Jellyfin.Server
         {
             services.AddResponseCompression();
             services.AddHttpContextAccessor();
-            services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), _applicationHost.GetApiPluginAssemblies());
+            services.AddJellyfinApi(
+                _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'),
+                _serverApplicationHost.GetApiPluginAssemblies());
 
             services.AddJellyfinApiSwagger();
 
@@ -53,7 +57,9 @@ namespace Jellyfin.Server
 
             services.AddJellyfinApiAuthorization();
 
-            var productHeader = new ProductInfoHeaderValue(_applicationHost.Name.Replace(' ', '-'), _applicationHost.ApplicationVersionString);
+            var productHeader = new ProductInfoHeaderValue(
+                _serverApplicationHost.Name.Replace(' ', '-'),
+                _serverApplicationHost.ApplicationVersionString);
             services
                 .AddHttpClient(NamedClient.Default, c =>
                 {
@@ -64,7 +70,7 @@ namespace Jellyfin.Server
             services.AddHttpClient(NamedClient.MusicBrainz, c =>
                 {
                     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());
         }
@@ -93,7 +99,11 @@ namespace Jellyfin.Server
 
             app.UseResponseCompression();
 
-            // TODO app.UseMiddleware<WebSocketMiddleware>();
+            if (_serverConfigurationManager.Configuration.RequireHttps
+                && _serverApplicationHost.ListenWithHttps)
+            {
+                app.UseHttpsRedirection();
+            }
 
             app.UseAuthentication();
             app.UseJellyfinApiSwagger(_serverConfigurationManager);
@@ -106,6 +116,8 @@ namespace Jellyfin.Server
                 app.UseHttpMetrics();
             }
 
+            app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
+
             app.UseEndpoints(endpoints =>
             {
                 endpoints.MapControllers();
@@ -115,8 +127,6 @@ namespace Jellyfin.Server
                 }
             });
 
-            app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
-
             // Add type descriptor for legacy datetime parsing.
             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>
         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))
             {
                 return cachedRemoteIp;

+ 4 - 3
MediaBrowser.Controller/Net/IHttpServer.cs

@@ -35,9 +35,10 @@ namespace MediaBrowser.Controller.Net
         /// <summary>
         /// The HTTP request handler.
         /// </summary>
-        /// <param name="context"></param>
-        /// <returns></returns>
-        Task RequestHandler(HttpContext context);
+        /// <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.