diff --git a/build/Version.props b/build/Version.props
index 3135589409..619f513bf4 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -3,7 +3,7 @@
- 6.15.102
+ 6.15.104
5.6.0
10.13.0
0.6.0
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index d292e85c70..bf95511814 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -643,7 +643,7 @@ namespace Tgstation.Server.Host.Core
global::System.Net.IPAddress.Any,
0));
- applicationBuilder.UseMiddleware(Options.Create(forwardedHeaderOptions));
+ applicationBuilder.UseForwardedHeaders(forwardedHeaderOptions);
applicationBuilder.Use((context, next) =>
{
diff --git a/src/Tgstation.Server.Host/Core/ForwardedHeadersMiddleware.cs b/src/Tgstation.Server.Host/Core/ForwardedHeadersMiddleware.cs
deleted file mode 100644
index a128a24e8f..0000000000
--- a/src/Tgstation.Server.Host/Core/ForwardedHeadersMiddleware.cs
+++ /dev/null
@@ -1,512 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-#pragma warning disable
-using System;
-using System.Buffers;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Runtime.CompilerServices;
-using System.Threading.Tasks;
-
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.HttpOverrides;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using Microsoft.Extensions.Primitives;
-
-namespace Tgstation.Server.Host.Core;
-
-///
-/// A middleware for forwarding proxied headers onto the current request.
-///
-public class ForwardedHeadersMiddleware
-{
- private readonly ForwardedHeadersOptions _options;
- private readonly RequestDelegate _next;
- private readonly ILogger _logger;
- private bool _allowAllHosts;
- private IList? _allowedHosts;
-
- // RFC 3986 scheme = ALPHA * (ALPHA / DIGIT / "+" / "-" / ".")
- private static readonly SearchValues SchemeChars =
- SearchValues.Create("+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
-
- // Host Matches Http.Sys and Kestrel
- // Host Matches RFC 3986 except "*" / "+" / "," / ";" / "=" and "%" HEXDIG HEXDIG which are not allowed by Http.Sys
- private static readonly SearchValues HostChars =
- SearchValues.Create("!$&'()-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~");
-
- // 0-9 / A-F / a-f / ":" / "."
- private static readonly SearchValues Ipv6HostChars =
- SearchValues.Create(".0123456789:ABCDEFabcdef");
-
- ///
- /// Create a new .
- ///
- /// The representing the next middleware in the pipeline.
- /// The used for logging.
- /// The for configuring the middleware.
- public ForwardedHeadersMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions options)
- {
- ArgumentNullException.ThrowIfNull(next);
- ArgumentNullException.ThrowIfNull(loggerFactory);
- ArgumentNullException.ThrowIfNull(options);
-
- // Make sure required options is not null or whitespace
- EnsureOptionNotNullorWhitespace(options.Value.ForwardedForHeaderName, nameof(options.Value.ForwardedForHeaderName));
- EnsureOptionNotNullorWhitespace(options.Value.ForwardedHostHeaderName, nameof(options.Value.ForwardedHostHeaderName));
- EnsureOptionNotNullorWhitespace(options.Value.ForwardedProtoHeaderName, nameof(options.Value.ForwardedProtoHeaderName));
- EnsureOptionNotNullorWhitespace(options.Value.ForwardedPrefixHeaderName, nameof(options.Value.ForwardedPrefixHeaderName));
- EnsureOptionNotNullorWhitespace(options.Value.OriginalForHeaderName, nameof(options.Value.OriginalForHeaderName));
- EnsureOptionNotNullorWhitespace(options.Value.OriginalHostHeaderName, nameof(options.Value.OriginalHostHeaderName));
- EnsureOptionNotNullorWhitespace(options.Value.OriginalProtoHeaderName, nameof(options.Value.OriginalProtoHeaderName));
- EnsureOptionNotNullorWhitespace(options.Value.OriginalPrefixHeaderName, nameof(options.Value.OriginalPrefixHeaderName));
-
- _options = options.Value;
- _logger = loggerFactory.CreateLogger();
- _next = next;
-
- PreProcessHosts();
-
- static void EnsureOptionNotNullorWhitespace(string value, string propertyName)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- throw new ArgumentException($"options.{propertyName} is required", nameof(options));
- }
- }
- }
-
- private void PreProcessHosts()
- {
- if (_options.AllowedHosts == null || _options.AllowedHosts.Count == 0)
- {
- _allowAllHosts = true;
- return;
- }
-
- var allowedHosts = new List();
- foreach (var entry in _options.AllowedHosts)
- {
- // Punycode. Http.Sys requires you to register Unicode hosts, but the headers contain punycode.
- var host = new HostString(entry).ToUriComponent();
-
- if (IsTopLevelWildcard(host))
- {
- // Disable filtering
- _allowAllHosts = true;
- return;
- }
-
- if (!allowedHosts.Contains(host, StringSegmentComparer.OrdinalIgnoreCase))
- {
- allowedHosts.Add(host);
- }
- }
-
- _allowedHosts = allowedHosts;
- }
-
- private static bool IsTopLevelWildcard(string host)
- {
- return (string.Equals("*", host, StringComparison.Ordinal) // HttpSys wildcard
- || string.Equals("[::]", host, StringComparison.Ordinal) // Kestrel wildcard, IPv6 Any
- || string.Equals("0.0.0.0", host, StringComparison.Ordinal)); // IPv4 Any
- }
-
- ///
- /// Executes the middleware.
- ///
- /// The for the current request.
- public Task Invoke(HttpContext context)
- {
- _logger.LogDebug("Pre apply");
- ApplyForwarders(context);
- _logger.LogDebug("Post apply");
- return _next(context);
- }
-
- ///
- /// Forward the proxied headers to the given .
- ///
- /// The .
- public void ApplyForwarders(HttpContext context)
- {
- // Gather expected headers.
- string[]? forwardedFor = null, forwardedProto = null, forwardedHost = null, forwardedPrefix = null;
- bool checkFor = false, checkProto = false, checkHost = false, checkPrefix = false;
- int entryCount = 0;
-
- var request = context.Request;
- var requestHeaders = context.Request.Headers;
- if (_options.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedFor))
- {
- checkFor = true;
- forwardedFor = requestHeaders.GetCommaSeparatedValues(_options.ForwardedForHeaderName);
- entryCount = Math.Max(forwardedFor.Length, entryCount);
- }
-
- if (_options.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedProto))
- {
- checkProto = true;
- forwardedProto = requestHeaders.GetCommaSeparatedValues(_options.ForwardedProtoHeaderName);
- _logger.LogDebug("Checking proto: {forwarded}", forwardedProto);
- if (_options.RequireHeaderSymmetry && checkFor && forwardedFor!.Length != forwardedProto.Length)
- {
- _logger.LogWarning(1, "Parameter count mismatch between X-Forwarded-For and X-Forwarded-Proto.");
- return;
- }
- entryCount = Math.Max(forwardedProto.Length, entryCount);
- }
-
- if (_options.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedHost))
- {
- checkHost = true;
- forwardedHost = requestHeaders.GetCommaSeparatedValues(_options.ForwardedHostHeaderName);
- if (_options.RequireHeaderSymmetry
- && ((checkFor && forwardedFor!.Length != forwardedHost.Length)
- || (checkProto && forwardedProto!.Length != forwardedHost.Length)))
- {
- _logger.LogWarning(1, "Parameter count mismatch between X-Forwarded-Host and X-Forwarded-For or X-Forwarded-Proto.");
- return;
- }
- entryCount = Math.Max(forwardedHost.Length, entryCount);
- }
-
- if (_options.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedPrefix))
- {
- checkPrefix = true;
- forwardedPrefix = requestHeaders.GetCommaSeparatedValues(_options.ForwardedPrefixHeaderName);
- if (_options.RequireHeaderSymmetry
- && ((checkFor && forwardedFor!.Length != forwardedPrefix.Length)
- || (checkProto && forwardedProto!.Length != forwardedPrefix.Length)
- || (checkHost && forwardedHost!.Length != forwardedPrefix.Length)))
- {
- _logger.LogWarning(1, "Parameter count mismatch between X-Forwarded-Prefix and X-Forwarded-Host and X-Forwarded-For or X-Forwarded-Proto.");
- return;
- }
- entryCount = Math.Max(forwardedPrefix.Length, entryCount);
- }
-
- // Apply ForwardLimit, if any
- if (_options.ForwardLimit.HasValue && entryCount > _options.ForwardLimit)
- {
- entryCount = _options.ForwardLimit.Value;
- }
-
- // Group the data together.
- var sets = new SetOfForwarders[entryCount];
- for (int i = 0; i < sets.Length; i++)
- {
- _logger.LogDebug("Set iter: {i}", i);
- // They get processed in reverse order, right to left.
- var set = new SetOfForwarders();
- if (checkFor && i < forwardedFor!.Length)
- {
- set.IpAndPortText = forwardedFor[forwardedFor.Length - i - 1];
- }
- if (checkProto && i < forwardedProto!.Length)
- {
- set.Scheme = forwardedProto[forwardedProto.Length - i - 1];
- _logger.LogDebug("Set scheme: {scheme}", set.Scheme);
- }
- if (checkHost && i < forwardedHost!.Length)
- {
- set.Host = forwardedHost[forwardedHost.Length - i - 1];
- }
- if (checkPrefix && i < forwardedPrefix!.Length)
- {
- set.Prefix = forwardedPrefix[forwardedPrefix.Length - i - 1];
- }
- sets[i] = set;
- }
-
- // Gather initial values
- var connection = context.Connection;
- var currentValues = new SetOfForwarders()
- {
- RemoteIpAndPort = connection.RemoteIpAddress != null ? new IPEndPoint(connection.RemoteIpAddress, connection.RemotePort) : null,
- // Host and Scheme initial values are never inspected, no need to set them here.
- };
-
- var checkKnownIps = _options.KnownNetworks.Count > 0 || _options.KnownProxies.Count > 0;
- bool applyChanges = false;
- int entriesConsumed = 0;
-
- for (; entriesConsumed < sets.Length; entriesConsumed++)
- {
- _logger.LogDebug("Consume iter: {i}", entriesConsumed);
- var set = sets[entriesConsumed];
- if (checkFor)
- {
- // For the first instance, allow remoteIp to be null for servers that don't support it natively.
- if (currentValues.RemoteIpAndPort != null && checkKnownIps && !CheckKnownAddress(currentValues.RemoteIpAndPort.Address))
- {
- // Stop at the first unknown remote IP, but still apply changes processed so far.
- if (_logger.IsEnabled(LogLevel.Debug))
- {
- _logger.LogDebug(1, "Unknown proxy: {RemoteIpAndPort}", currentValues.RemoteIpAndPort);
- }
- break;
- }
-
- if (IPEndPoint.TryParse(set.IpAndPortText, out var parsedEndPoint))
- {
- applyChanges = true;
- set.RemoteIpAndPort = parsedEndPoint;
- currentValues.IpAndPortText = set.IpAndPortText;
- currentValues.RemoteIpAndPort = set.RemoteIpAndPort;
- }
- else if (!string.IsNullOrEmpty(set.IpAndPortText))
- {
- // Stop at the first unparsable IP, but still apply changes processed so far.
- if (_logger.IsEnabled(LogLevel.Debug))
- {
- _logger.LogDebug(1, "Unparsable IP: {IpAndPortText}", set.IpAndPortText);
- }
- break;
- }
- else if (_options.RequireHeaderSymmetry)
- {
- _logger.LogWarning(2, "Missing forwarded IPAddress.");
- return;
- }
- }
-
- if (checkProto)
- {
- _logger.LogDebug("Consume check proto: {setScheme}", set.Scheme);
- if (!string.IsNullOrEmpty(set.Scheme) && set.Scheme.AsSpan().IndexOfAnyExcept(SchemeChars) < 0)
- {
- _logger.LogDebug("Consume apply proto");
- applyChanges = true;
- currentValues.Scheme = set.Scheme;
- }
- else if (_options.RequireHeaderSymmetry)
- {
- _logger.LogWarning(3, $"Forwarded scheme is not present, this is required by {nameof(_options.RequireHeaderSymmetry)}");
- return;
- }
- }
-
- if (checkHost)
- {
- if (!string.IsNullOrEmpty(set.Host) && TryValidateHost(set.Host)
- && (_allowAllHosts || HostString.MatchesAny(set.Host, _allowedHosts!)))
- {
- applyChanges = true;
- currentValues.Host = set.Host;
- }
- else if (_options.RequireHeaderSymmetry)
- {
- _logger.LogWarning(4, $"Incorrect number of x-forwarded-host header values, see {nameof(_options.RequireHeaderSymmetry)}.");
- return;
- }
- }
-
- if (checkPrefix)
- {
- if (!string.IsNullOrEmpty(set.Prefix) && set.Prefix[0] == '/')
- {
- applyChanges = true;
- currentValues.Prefix = set.Prefix;
- }
- else if (_options.RequireHeaderSymmetry)
- {
- _logger.LogWarning(5, $"Incorrect number of x-forwarded-prefix header values, see {nameof(_options.RequireHeaderSymmetry)}");
- return;
- }
- }
- }
-
- if (applyChanges)
- {
- _logger.LogDebug("Apply changes: {scheme}", currentValues.Scheme);
- if (checkFor && currentValues.RemoteIpAndPort != null)
- {
- if (connection.RemoteIpAddress != null)
- {
- // Save the original
- requestHeaders[_options.OriginalForHeaderName] = new IPEndPoint(connection.RemoteIpAddress, connection.RemotePort).ToString();
- }
- if (forwardedFor!.Length > entriesConsumed)
- {
- // Truncate the consumed header values
- requestHeaders[_options.ForwardedForHeaderName] =
- TruncateConsumedHeaderValues(forwardedFor, entriesConsumed);
- }
- else
- {
- // All values were consumed
- requestHeaders.Remove(_options.ForwardedForHeaderName);
- }
- connection.RemoteIpAddress = currentValues.RemoteIpAndPort.Address;
- connection.RemotePort = currentValues.RemoteIpAndPort.Port;
- }
-
- if (checkProto && currentValues.Scheme != null)
- {
- // Save the original
- requestHeaders[_options.OriginalProtoHeaderName] = request.Scheme;
- if (forwardedProto!.Length > entriesConsumed)
- {
- // Truncate the consumed header values
- requestHeaders[_options.ForwardedProtoHeaderName] =
- TruncateConsumedHeaderValues(forwardedProto, entriesConsumed);
- }
- else
- {
- // All values were consumed
- requestHeaders.Remove(_options.ForwardedProtoHeaderName);
- }
- request.Scheme = currentValues.Scheme;
-
- _logger.LogDebug("Do apply proto: {scheme}, {forwarded}", request.Scheme, forwardedProto);
- }
-
- if (checkHost && currentValues.Host != null)
- {
- // Save the original
- requestHeaders[_options.OriginalHostHeaderName] = request.Host.ToString();
- if (forwardedHost!.Length > entriesConsumed)
- {
- // Truncate the consumed header values
- requestHeaders[_options.ForwardedHostHeaderName] =
- TruncateConsumedHeaderValues(forwardedHost, entriesConsumed);
- }
- else
- {
- // All values were consumed
- requestHeaders.Remove(_options.ForwardedHostHeaderName);
- }
- request.Host = HostString.FromUriComponent(currentValues.Host);
- }
-
- if (checkPrefix && currentValues.Prefix != null)
- {
- if (request.PathBase.HasValue)
- {
- // Save the original
- requestHeaders[_options.OriginalPrefixHeaderName] = request.PathBase.ToString();
- }
-
- if (forwardedPrefix!.Length > entriesConsumed)
- {
- // Truncate the consumed header values
- requestHeaders[_options.ForwardedPrefixHeaderName] =
- TruncateConsumedHeaderValues(forwardedPrefix, entriesConsumed);
- }
- else
- {
- // All values were consumed
- requestHeaders.Remove(_options.ForwardedPrefixHeaderName);
- }
-
- request.PathBase = PathString.FromUriComponent(currentValues.Prefix);
- }
- }
- }
-
- private bool CheckKnownAddress(IPAddress address)
- {
- if (address.IsIPv4MappedToIPv6)
- {
- var ipv4Address = address.MapToIPv4();
- if (CheckKnownAddress(ipv4Address))
- {
- return true;
- }
- }
- if (_options.KnownProxies.Contains(address))
- {
- return true;
- }
- foreach (var network in _options.KnownNetworks)
- {
- if (network.Contains(address))
- {
- return true;
- }
- }
- return false;
- }
-
- private struct SetOfForwarders
- {
- public string IpAndPortText;
- public IPEndPoint? RemoteIpAndPort;
- public string Host;
- public string Scheme;
- public string Prefix;
- }
-
- // Empty was checked for by the caller
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool TryValidateHost(string host)
- {
- if (host[0] == '[')
- {
- return TryValidateIPv6Host(host);
- }
-
- if (host[0] == ':')
- {
- // Only a port
- return false;
- }
-
- var firstNonHostCharIdx = host.AsSpan().IndexOfAnyExcept(HostChars);
- if (firstNonHostCharIdx == -1)
- {
- // no port
- return true;
- }
- else
- {
- return TryValidateHostPort(host, firstNonHostCharIdx);
- }
- }
-
- // The lead '[' was already checked
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool TryValidateIPv6Host(string hostText)
- {
- var host = hostText.AsSpan(1);
-
- var hostEndIdx = host.IndexOfAnyExcept(Ipv6HostChars);
- if ((uint)hostEndIdx >= (uint)host.Length || // No ']'. The uint cast is there to eliminate the
- // bounds check on the 'host[hostEndIdx]' access below.
- host[hostEndIdx] != ']' || // We found an invalid host character
- hostEndIdx < 3) // [::1] is the shortest valid IPv6 host
- {
- return false;
- }
-
- // If there's nothing left, we're good. If there's more, validate it as a port.
- // +2 to skip the '[' and ']' (the '[' wasn't included in hostEndIdx because we
- // cut it off in the AsSpan above).
- return (hostEndIdx + 2 == hostText.Length) || TryValidateHostPort(hostText, hostEndIdx + 2);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool TryValidateHostPort(string hostText, int offset)
- {
- if (hostText[offset] != ':' || hostText.Length == offset + 1)
- {
- // Must have at least one number after the colon if present.
- return false;
- }
-
- return hostText.AsSpan(offset + 1).IndexOfAnyExceptInRange('0', '9') < 0;
- }
-
- private static string[] TruncateConsumedHeaderValues(string[] forwarded, int entriesConsumed)
- {
- var newLength = forwarded.Length - entriesConsumed;
- var remaining = new string[newLength];
- Array.Copy(forwarded, remaining, newLength);
- return remaining;
- }
-}