aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server/Middleware
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server/Middleware')
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs84
-rw-r--r--Jellyfin.Server/Middleware/ExceptionMiddleware.cs8
-rw-r--r--Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs50
-rw-r--r--Jellyfin.Server/Middleware/LanFilteringMiddleware.cs45
-rw-r--r--Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs54
-rw-r--r--Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs35
-rw-r--r--Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs3
-rw-r--r--Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs47
-rw-r--r--Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs51
-rw-r--r--Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs91
-rw-r--r--Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs40
11 files changed, 501 insertions, 7 deletions
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
new file mode 100644
index 0000000000..3e5982eedf
--- /dev/null
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -0,0 +1,84 @@
+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 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.GetNetworkConfiguration().BaseUrl;
+
+ if (!string.IsNullOrEmpty(baseUrlPrefix))
+ {
+ var startsWithBaseUrl = localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase);
+
+ if (!startsWithBaseUrl
+ && (localPath.Equals("/health", StringComparison.OrdinalIgnoreCase)
+ || localPath.Equals("/health/", StringComparison.OrdinalIgnoreCase)))
+ {
+ _logger.LogDebug("Redirecting /health check");
+ httpContext.Response.Redirect(baseUrlPrefix + "/health");
+ return;
+ }
+
+ if (!startsWithBaseUrl
+ || localPath.Length == baseUrlPrefix.Length
+ // Local path is /baseUrl/
+ || (localPath.Length == baseUrlPrefix.Length + 1 && localPath[^1] == '/'))
+ {
+ // Always redirect back to the default path if the base prefix is invalid, missing, or is the full path.
+ _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
+ httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ return;
+ }
+ }
+ else if (string.IsNullOrEmpty(localPath)
+ || localPath.Equals("/", StringComparison.Ordinal))
+ {
+ // Always redirect back to the default path if root is requested.
+ _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
+ httpContext.Response.Redirect("/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ return;
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index 63effafc19..db7877c31e 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -125,7 +125,8 @@ namespace Jellyfin.Server.Middleware
switch (ex)
{
case ArgumentException _: return StatusCodes.Status400BadRequest;
- case SecurityException _: return StatusCodes.Status401Unauthorized;
+ case AuthenticationException _: return StatusCodes.Status401Unauthorized;
+ case SecurityException _: return StatusCodes.Status403Forbidden;
case DirectoryNotFoundException _:
case FileNotFoundException _:
case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
@@ -136,11 +137,6 @@ namespace Jellyfin.Server.Middleware
private string NormalizeExceptionMessage(string msg)
{
- if (msg == null)
- {
- return string.Empty;
- }
-
// Strip any information we don't want to reveal
return msg.Replace(
_configuration.ApplicationPaths.ProgramSystemPath,
diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
new file mode 100644
index 0000000000..0afcd61a05
--- /dev/null
+++ b/Jellyfin.Server/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.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>
+ /// <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.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
new file mode 100644
index 0000000000..67bf24d2a5
--- /dev/null
+++ b/Jellyfin.Server/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.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 host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
+
+ if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
+ {
+ return;
+ }
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
new file mode 100644
index 0000000000..fdd8974d2b
--- /dev/null
+++ b/Jellyfin.Server/Middleware/LegacyEmbyRouteRewriteMiddleware.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.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);
+ }
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
new file mode 100644
index 0000000000..fd0ebbf438
--- /dev/null
+++ b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
@@ -0,0 +1,35 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Jellyfin.Server.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)
+ {
+ httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(httpContext.Features.Get<IQueryFeature>()));
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
index 3122d92cbc..74874da1b0 100644
--- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
@@ -1,6 +1,7 @@
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;
@@ -69,7 +70,7 @@ namespace Jellyfin.Server.Middleware
_logger.LogWarning(
"Slow HTTP Response from {url} to {remoteIp} in {elapsed:g} with Status Code {statusCode}",
context.Request.GetDisplayUrl(),
- context.Connection.RemoteIpAddress,
+ context.GetNormalizedRemoteIp(),
watch.Elapsed,
context.Response.StatusCode);
}
diff --git a/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs
new file mode 100644
index 0000000000..9d40d74fe9
--- /dev/null
+++ b/Jellyfin.Server/Middleware/RobotsRedirectionMiddleware.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.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);
+ }
+ }
+} \ No newline at end of file
diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
new file mode 100644
index 0000000000..2ec0633924
--- /dev/null
+++ b/Jellyfin.Server/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.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
+ || 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.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
new file mode 100644
index 0000000000..c1f5b5dfaf
--- /dev/null
+++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using Jellyfin.Extensions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+
+namespace Jellyfin.Server.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;
+ }
+
+ // Unencode and re-parse querystring.
+ var unencodedKey = HttpUtility.UrlDecode(key);
+
+ if (string.Equals(unencodedKey, key, StringComparison.Ordinal))
+ {
+ // Don't do anything if it's not encoded.
+ _store = value;
+ return;
+ }
+
+ var pairs = new Dictionary<string, StringValues>();
+ var queryString = unencodedKey.SpanSplit('&');
+
+ foreach (var pair in queryString)
+ {
+ 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.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
new file mode 100644
index 0000000000..b7a5d2b346
--- /dev/null
+++ b/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);
+ }
+ }
+}