diff options
| author | Patrick Barron <barronpm@gmail.com> | 2023-01-13 21:37:37 -0500 |
|---|---|---|
| committer | Patrick Barron <barronpm@gmail.com> | 2023-01-15 15:46:50 -0500 |
| commit | 74a07f6d1c52533aa648da27cb3924d6bb41bb19 (patch) | |
| tree | 97749cf5ca4265968e2182b94ea098dff0aaf4e2 /Jellyfin.Api/Middleware | |
| parent | be206d4ff2d799296764c005d7bbbdb5cff7ee2c (diff) | |
Move Middleware to Jellyfin.Api
Diffstat (limited to 'Jellyfin.Api/Middleware')
11 files changed, 711 insertions, 0 deletions
diff --git a/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs new file mode 100644 index 000000000..6bd9e0b08 --- /dev/null +++ b/Jellyfin.Api/Middleware/BaseUrlRedirectionMiddleware.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; + +namespace Jellyfin.Api.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.GetNetworkConfiguration().BaseUrl; + + if (string.IsNullOrEmpty(localPath) + || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, baseUrlPrefix + "/web", StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, baseUrlPrefix + "/web/", StringComparison.OrdinalIgnoreCase) + || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase) + ) + { + // Redirect health endpoint + if (string.Equals(localPath, "/health", StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, "/health/", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("Redirecting /health check"); + httpContext.Response.Redirect(baseUrlPrefix + "/health"); + return; + } + + // Always redirect back to the default path if the base prefix is invalid or missing + _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); + + var port = httpContext.Request.Host.Port ?? -1; + var uri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, localPath).Uri; + var redirectUri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]).Uri; + var target = uri.MakeRelativeUri(redirectUri).ToString(); + _logger.LogDebug("Redirecting to {Target}", target); + + httpContext.Response.Redirect(target); + return; + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs new file mode 100644 index 000000000..6b3aeb187 --- /dev/null +++ b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs @@ -0,0 +1,151 @@ +using System; +using System.IO; +using System.Net.Mime; +using System.Net.Sockets; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Middleware +{ + /// <summary> + /// Exception Middleware. + /// </summary> + public class ExceptionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger<ExceptionMiddleware> _logger; + private readonly IServerConfigurationManager _configuration; + private readonly IWebHostEnvironment _hostEnvironment; + + /// <summary> + /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class. + /// </summary> + /// <param name="next">Next request delegate.</param> + /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="hostEnvironment">Instance of the <see cref="IWebHostEnvironment"/> interface.</param> + public ExceptionMiddleware( + RequestDelegate next, + ILogger<ExceptionMiddleware> logger, + IServerConfigurationManager serverConfigurationManager, + IWebHostEnvironment hostEnvironment) + { + _next = next; + _logger = logger; + _configuration = serverConfigurationManager; + _hostEnvironment = hostEnvironment; + } + + /// <summary> + /// Invoke request. + /// </summary> + /// <param name="context">Request context.</param> + /// <returns>Task.</returns> + public async Task Invoke(HttpContext context) + { + try + { + await _next(context).ConfigureAwait(false); + } + catch (Exception ex) + { + if (context.Response.HasStarted) + { + _logger.LogWarning("The response has already started, the exception middleware will not be executed."); + throw; + } + + ex = GetActualException(ex); + + bool ignoreStackTrace = + ex is SocketException + || ex is IOException + || ex is OperationCanceledException + || ex is SecurityException + || ex is AuthenticationException + || ex is FileNotFoundException; + + if (ignoreStackTrace) + { + _logger.LogError( + "Error processing request: {ExceptionMessage}. URL {Method} {Url}.", + ex.Message.TrimEnd('.'), + context.Request.Method, + context.Request.Path); + } + else + { + _logger.LogError( + ex, + "Error processing request. URL {Method} {Url}.", + context.Request.Method, + context.Request.Path); + } + + context.Response.StatusCode = GetStatusCode(ex); + context.Response.ContentType = MediaTypeNames.Text.Plain; + + // Don't send exception unless the server is in a Development environment + var errorContent = _hostEnvironment.IsDevelopment() + ? NormalizeExceptionMessage(ex.Message) + : "Error processing request."; + await context.Response.WriteAsync(errorContent).ConfigureAwait(false); + } + } + + private static Exception GetActualException(Exception ex) + { + if (ex is AggregateException agg) + { + var inner = agg.InnerException; + if (inner is not null) + { + return GetActualException(inner); + } + + var inners = agg.InnerExceptions; + if (inners.Count > 0) + { + return GetActualException(inners[0]); + } + } + + return ex; + } + + private static int GetStatusCode(Exception ex) + { + switch (ex) + { + case ArgumentException _: return StatusCodes.Status400BadRequest; + case AuthenticationException _: return StatusCodes.Status401Unauthorized; + case SecurityException _: return StatusCodes.Status403Forbidden; + case DirectoryNotFoundException _: + case FileNotFoundException _: + case ResourceNotFoundException _: return StatusCodes.Status404NotFound; + case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed; + default: return StatusCodes.Status500InternalServerError; + } + } + + private string NormalizeExceptionMessage(string msg) + { + // Strip any information we don't want to reveal + return msg.Replace( + _configuration.ApplicationPaths.ProgramSystemPath, + string.Empty, + StringComparison.OrdinalIgnoreCase) + .Replace( + _configuration.ApplicationPaths.ProgramDataPath, + string.Empty, + StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs new file mode 100644 index 000000000..f7af91e48 --- /dev/null +++ b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs @@ -0,0 +1,50 @@ +using System.Net; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.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> + /// <returns>The async task.</returns> + public async Task Invoke(HttpContext httpContext, INetworkManager networkManager) + { + if (httpContext.IsLocal()) + { + // Running locally. + await _next(httpContext).ConfigureAwait(false); + return; + } + + var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; + + if (!networkManager.HasRemoteAccess(remoteIp)) + { + return; + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs new file mode 100644 index 000000000..18f13bbce --- /dev/null +++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs @@ -0,0 +1,45 @@ +using System.Net; +using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.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 host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; + + if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) + { + return; + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs new file mode 100644 index 000000000..b73923c1e --- /dev/null +++ b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Middleware +{ + /// <summary> + /// Removes /emby and /mediabrowser from requested route. + /// </summary> + public class LegacyEmbyRouteRewriteMiddleware + { + private const string EmbyPath = "/emby"; + private const string MediabrowserPath = "/mediabrowser"; + + private readonly RequestDelegate _next; + private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + /// <param name="logger">The logger.</param> + public LegacyEmbyRouteRewriteMiddleware( + RequestDelegate next, + ILogger<LegacyEmbyRouteRewriteMiddleware> logger) + { + _next = next; + _logger = logger; + } + + /// <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) + { + var localPath = httpContext.Request.Path.ToString(); + if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase)) + { + httpContext.Request.Path = localPath[EmbyPath.Length..]; + _logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath); + } + else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase)) + { + httpContext.Request.Path = localPath[MediabrowserPath.Length..]; + _logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath); + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs new file mode 100644 index 000000000..4b6304e0e --- /dev/null +++ b/Jellyfin.Api/Middleware/QueryStringDecodingMiddleware.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Jellyfin.Api.Middleware +{ + /// <summary> + /// URL decodes the querystring before binding. + /// </summary> + public class QueryStringDecodingMiddleware + { + private readonly RequestDelegate _next; + + /// <summary> + /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + public QueryStringDecodingMiddleware(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) + { + var feature = httpContext.Features.Get<IQueryFeature>(); + if (feature is not null) + { + httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(feature)); + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs new file mode 100644 index 000000000..3701d0f45 --- /dev/null +++ b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs @@ -0,0 +1,69 @@ +using System.Diagnostics; +using System.Globalization; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Middleware +{ + /// <summary> + /// Response time middleware. + /// </summary> + public class ResponseTimeMiddleware + { + private const string ResponseHeaderResponseTime = "X-Response-Time-ms"; + + private readonly RequestDelegate _next; + private readonly ILogger<ResponseTimeMiddleware> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class. + /// </summary> + /// <param name="next">Next request delegate.</param> + /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param> + public ResponseTimeMiddleware( + RequestDelegate next, + ILogger<ResponseTimeMiddleware> logger) + { + _next = next; + _logger = logger; + } + + /// <summary> + /// Invoke request. + /// </summary> + /// <param name="context">Request context.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <returns>Task.</returns> + public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager) + { + var startTimestamp = Stopwatch.GetTimestamp(); + + var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning; + var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs; + context.Response.OnStarting(() => + { + var responseTime = Stopwatch.GetElapsedTime(startTimestamp); + var responseTimeMs = responseTime.TotalMilliseconds; + if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}", + context.Request.GetDisplayUrl(), + context.GetNormalizedRemoteIp(), + responseTime, + context.Response.StatusCode); + } + + context.Response.Headers[ResponseHeaderResponseTime] = responseTimeMs.ToString(CultureInfo.InvariantCulture); + return Task.CompletedTask; + }); + + // Call the next delegate/middleware in the pipeline + await this._next(context).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs new file mode 100644 index 000000000..2e69580be --- /dev/null +++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Middleware +{ + /// <summary> + /// Redirect requests to robots.txt to web/robots.txt. + /// </summary> + public class RobotsRedirectionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger<RobotsRedirectionMiddleware> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + /// <param name="logger">The logger.</param> + public RobotsRedirectionMiddleware( + RequestDelegate next, + ILogger<RobotsRedirectionMiddleware> logger) + { + _next = next; + _logger = logger; + } + + /// <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) + { + var localPath = httpContext.Request.Path.ToString(); + if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("Redirecting robots.txt request to web/robots.txt"); + httpContext.Response.Redirect("web/robots.txt"); + return; + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs new file mode 100644 index 000000000..dcd64401a --- /dev/null +++ b/Jellyfin.Api/Middleware/ServerStartupMessageMiddleware.cs @@ -0,0 +1,51 @@ +using System; +using System.Net.Mime; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Model.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.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 + || httpContext.Request.Path.Equals("/system/ping", StringComparison.OrdinalIgnoreCase)) + { + 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); + } + } +} diff --git a/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs new file mode 100644 index 000000000..d35e0fcfd --- /dev/null +++ b/Jellyfin.Api/Middleware/UrlDecodeQueryFeature.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jellyfin.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Jellyfin.Api.Middleware +{ + /// <summary> + /// Defines the <see cref="UrlDecodeQueryFeature"/>. + /// </summary> + public class UrlDecodeQueryFeature : IQueryFeature + { + private IQueryCollection? _store; + + /// <summary> + /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class. + /// </summary> + /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param> + public UrlDecodeQueryFeature(IQueryFeature feature) + { + Query = feature.Query; + } + + /// <summary> + /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>. + /// </summary> + public IQueryCollection Query + { + get + { + return _store ?? QueryCollection.Empty; + } + + set + { + // Only interested in where the querystring is encoded which shows up as one key with nothing in the value. + if (value.Count != 1) + { + _store = value; + return; + } + + // Encoded querystrings have no value, so don't process anything if a value is present. + var (key, stringValues) = value.First(); + if (!string.IsNullOrEmpty(stringValues)) + { + _store = value; + return; + } + + if (!key.Contains('=', StringComparison.Ordinal)) + { + _store = value; + return; + } + + var pairs = new Dictionary<string, StringValues>(); + foreach (var pair in key.SpanSplit('&')) + { + var i = pair.IndexOf('='); + if (i == -1) + { + // encoded is an equals. + // We use TryAdd so duplicate keys get ignored + pairs.TryAdd(pair.ToString(), StringValues.Empty); + continue; + } + + var k = pair[..i].ToString(); + var v = pair[(i + 1)..].ToString(); + if (!pairs.TryAdd(k, new StringValues(v))) + { + pairs[k] = StringValues.Concat(pairs[k], v); + } + } + + _store = new QueryCollection(pairs); + } + } + } +} diff --git a/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs new file mode 100644 index 000000000..2cf1e5e4a --- /dev/null +++ b/Jellyfin.Api/Middleware/WebSocketHandlerMiddleware.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.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); + } + } +} |
