From d2d61b78e74709cecd5b5ddb792ef9a56fcf5301 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Wed, 19 Mar 2025 20:07:27 -0400 Subject: [PATCH] Carbonhell test 4 --- build/Version.props | 2 +- src/Tgstation.Server.Host/Core/Application.cs | 2 +- .../Core/ForwardedHeadersMiddleware.cs | 512 ------------------ 3 files changed, 2 insertions(+), 514 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Core/ForwardedHeadersMiddleware.cs 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; - } -}