Skip to content

Commit b446ab7

Browse files
authored
Add custom request header decoder API to Kestrel (#23233)
1 parent bfbb8b0 commit b446ab7

20 files changed

+336
-100
lines changed

src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ public KestrelServerOptions() { }
137137
public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
138138
public bool EnableAltSvc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
139139
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
140+
public System.Func<string, System.Text.Encoding> RequestHeaderEncodingSelector { get { throw null; } set { } }
140141
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; }
141142
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; }
142143
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config, bool reloadOnChange) { throw null; }

src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@ internal class ConfigurationReader
1818
private const string EndpointDefaultsKey = "EndpointDefaults";
1919
private const string EndpointsKey = "Endpoints";
2020
private const string UrlKey = "Url";
21-
private const string Latin1RequestHeadersKey = "Latin1RequestHeaders";
2221

2322
private readonly IConfiguration _configuration;
2423

2524
private IDictionary<string, CertificateConfig> _certificates;
2625
private EndpointDefaults _endpointDefaults;
2726
private IEnumerable<EndpointConfig> _endpoints;
28-
private bool? _latin1RequestHeaders;
2927

3028
public ConfigurationReader(IConfiguration configuration)
3129
{
@@ -35,7 +33,6 @@ public ConfigurationReader(IConfiguration configuration)
3533
public IDictionary<string, CertificateConfig> Certificates => _certificates ??= ReadCertificates();
3634
public EndpointDefaults EndpointDefaults => _endpointDefaults ??= ReadEndpointDefaults();
3735
public IEnumerable<EndpointConfig> Endpoints => _endpoints ??= ReadEndpoints();
38-
public bool Latin1RequestHeaders => _latin1RequestHeaders ??= _configuration.GetValue<bool>(Latin1RequestHeadersKey);
3936

4037
private IDictionary<string, CertificateConfig> ReadCertificates()
4138
{

src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs

Lines changed: 66 additions & 4 deletions
Large diffs are not rendered by default.

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ public void Reset()
369369
ConnectionIdFeature = ConnectionId;
370370

371371
HttpRequestHeaders.Reset();
372-
HttpRequestHeaders.UseLatin1 = ServerOptions.Latin1RequestHeaders;
372+
HttpRequestHeaders.EncodingSelector = ServerOptions.RequestHeaderEncodingSelector;
373373
HttpRequestHeaders.ReuseHeaderValues = !ServerOptions.DisableStringReuse;
374374
HttpResponseHeaders.Reset();
375375
RequestHeaders = HttpRequestHeaders;
@@ -532,7 +532,7 @@ public void OnTrailer(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
532532
}
533533

534534
string key = name.GetHeaderName();
535-
var valueStr = value.GetRequestHeaderStringNonNullCharacters(ServerOptions.Latin1RequestHeaders);
535+
var valueStr = value.GetRequestHeaderString(key, HttpRequestHeaders.EncodingSelector);
536536
RequestTrailers.Append(key, valueStr);
537537
}
538538

src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
using System.Buffers.Text;
66
using System.Collections;
77
using System.Collections.Generic;
8+
using System.Globalization;
89
using System.Runtime.CompilerServices;
10+
using System.Text;
911
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
1012
using Microsoft.Extensions.Primitives;
1113
using Microsoft.Net.Http.Headers;
@@ -17,12 +19,12 @@ internal sealed partial class HttpRequestHeaders : HttpHeaders
1719
private long _previousBits = 0;
1820

1921
public bool ReuseHeaderValues { get; set; }
20-
public bool UseLatin1 { get; set; }
22+
public Func<string, Encoding> EncodingSelector { get; set; }
2123

22-
public HttpRequestHeaders(bool reuseHeaderValues = true, bool useLatin1 = false)
24+
public HttpRequestHeaders(bool reuseHeaderValues = true, Func<string, Encoding> encodingSelector = null)
2325
{
2426
ReuseHeaderValues = reuseHeaderValues;
25-
UseLatin1 = useLatin1;
27+
EncodingSelector = encodingSelector ?? KestrelServerOptions.DefaultRequestHeaderEncodingSelector;
2628
}
2729

2830
public void OnHeadersComplete()
@@ -87,7 +89,30 @@ private void AppendContentLength(ReadOnlySpan<byte> value)
8789
parsed < 0 ||
8890
consumed != value.Length)
8991
{
90-
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetRequestHeaderStringNonNullCharacters(UseLatin1));
92+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetRequestHeaderString(HeaderNames.ContentLength, EncodingSelector));
93+
}
94+
95+
_contentLength = parsed;
96+
}
97+
98+
[MethodImpl(MethodImplOptions.NoInlining)]
99+
private void AppendContentLengthCustomEncoding(ReadOnlySpan<byte> value, Encoding customEncoding)
100+
{
101+
if (_contentLength.HasValue)
102+
{
103+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.MultipleContentLengths);
104+
}
105+
106+
// long.MaxValue = 9223372036854775807 (19 chars)
107+
Span<char> decodedChars = stackalloc char[20];
108+
var numChars = customEncoding.GetChars(value, decodedChars);
109+
long parsed = -1;
110+
111+
if (numChars > 19 ||
112+
!long.TryParse(decodedChars.Slice(0, numChars), NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ||
113+
parsed < 0)
114+
{
115+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetRequestHeaderString(HeaderNames.ContentLength, EncodingSelector));
91116
}
92117

93118
_contentLength = parsed;
@@ -108,11 +133,10 @@ private bool AddValueUnknown(string key, StringValues value)
108133
}
109134

110135
[MethodImpl(MethodImplOptions.NoInlining)]
111-
private unsafe void AppendUnknownHeaders(ReadOnlySpan<byte> name, string valueString)
136+
private unsafe void AppendUnknownHeaders(string name, string valueString)
112137
{
113-
string key = name.GetHeaderName();
114-
Unknown.TryGetValue(key, out var existing);
115-
Unknown[key] = AppendValue(existing, valueString);
138+
Unknown.TryGetValue(name, out var existing);
139+
Unknown[name] = AppendValue(existing, valueString);
116140
}
117141

118142
public Enumerator GetEnumerator()

src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal static partial class HttpUtilities
2727
private const ulong _http10VersionLong = 3471766442030158920; // GetAsciiStringAsLong("HTTP/1.0"); const results in better codegen
2828
private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
2929

30-
private static readonly UTF8EncodingSealed HeaderValueEncoding = new UTF8EncodingSealed();
30+
private static readonly UTF8EncodingSealed DefaultRequestHeaderEncoding = new UTF8EncodingSealed();
3131
private static readonly SpanAction<char, IntPtr> _getHeaderName = GetHeaderName;
3232
private static readonly SpanAction<char, IntPtr> _getAsciiStringNonNullCharacters = GetAsciiStringNonNullCharacters;
3333

@@ -120,11 +120,8 @@ public static unsafe string GetAsciiStringNonNullCharacters(this ReadOnlySpan<by
120120
}
121121
}
122122

123-
public static string GetAsciiOrUTF8StringNonNullCharacters(this Span<byte> span)
124-
=> GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan<byte>)span);
125-
126123
public static string GetAsciiOrUTF8StringNonNullCharacters(this ReadOnlySpan<byte> span)
127-
=> StringUtilities.GetAsciiOrUTF8StringNonNullCharacters(span, HeaderValueEncoding);
124+
=> StringUtilities.GetAsciiOrUTF8StringNonNullCharacters(span, DefaultRequestHeaderEncoding);
128125

129126
private static unsafe void GetAsciiStringNonNullCharacters(Span<char> buffer, IntPtr state)
130127
{
@@ -139,8 +136,34 @@ private static unsafe void GetAsciiStringNonNullCharacters(Span<char> buffer, In
139136
}
140137
}
141138

142-
public static string GetRequestHeaderStringNonNullCharacters(this ReadOnlySpan<byte> span, bool useLatin1) =>
143-
useLatin1 ? span.GetLatin1StringNonNullCharacters() : span.GetAsciiOrUTF8StringNonNullCharacters(HeaderValueEncoding);
139+
public static string GetRequestHeaderString(this ReadOnlySpan<byte> span, string name, Func<string, Encoding> encodingSelector)
140+
{
141+
if (ReferenceEquals(KestrelServerOptions.DefaultRequestHeaderEncodingSelector, encodingSelector))
142+
{
143+
return span.GetAsciiOrUTF8StringNonNullCharacters(DefaultRequestHeaderEncoding);
144+
}
145+
146+
var encoding = encodingSelector(name);
147+
148+
if (encoding is null)
149+
{
150+
return span.GetAsciiOrUTF8StringNonNullCharacters(DefaultRequestHeaderEncoding);
151+
}
152+
153+
if (ReferenceEquals(encoding, Encoding.Latin1))
154+
{
155+
return span.GetLatin1StringNonNullCharacters();
156+
}
157+
158+
try
159+
{
160+
return encoding.GetString(span);
161+
}
162+
catch (DecoderFallbackException ex)
163+
{
164+
throw new InvalidOperationException(ex.Message, ex);
165+
}
166+
}
144167

145168
public static string GetAsciiStringEscaped(this ReadOnlySpan<byte> span, int maxChars)
146169
{

src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,6 @@ public void Load()
255255

256256
ConfigurationReader = new ConfigurationReader(Configuration);
257257

258-
Options.Latin1RequestHeaders = ConfigurationReader.Latin1RequestHeaders;
259-
260258
LoadDefaultCert(ConfigurationReader);
261259

262260
foreach (var endpoint in ConfigurationReader.Endpoints)

src/Servers/Kestrel/Core/src/KestrelServer.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,19 @@ public class KestrelServer : IServer
3535

3636
private IDisposable _configChangedRegistration;
3737

38-
public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, ILoggerFactory loggerFactory)
38+
public KestrelServer(
39+
IOptions<KestrelServerOptions> options,
40+
IEnumerable<IConnectionListenerFactory> transportFactories,
41+
ILoggerFactory loggerFactory)
3942
: this(transportFactories, null, CreateServiceContext(options, loggerFactory))
4043
{
4144
}
4245

43-
public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories, ILoggerFactory loggerFactory)
46+
public KestrelServer(
47+
IOptions<KestrelServerOptions> options,
48+
IEnumerable<IConnectionListenerFactory> transportFactories,
49+
IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
50+
ILoggerFactory loggerFactory)
4451
: this(transportFactories, multiplexedFactories, CreateServiceContext(options, loggerFactory))
4552
{
4653
}
@@ -52,7 +59,10 @@ internal KestrelServer(IEnumerable<IConnectionListenerFactory> transportFactorie
5259
}
5360

5461
// For testing
55-
internal KestrelServer(IEnumerable<IConnectionListenerFactory> transportFactories, IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories, ServiceContext serviceContext)
62+
internal KestrelServer(
63+
IEnumerable<IConnectionListenerFactory> transportFactories,
64+
IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
65+
ServiceContext serviceContext)
5666
{
5767
if (transportFactories == null)
5868
{

src/Servers/Kestrel/Core/src/KestrelServerOptions.cs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Net;
99
using System.Security.Cryptography.X509Certificates;
10+
using System.Text;
1011
using Microsoft.AspNetCore.Certificates.Generation;
1112
using Microsoft.AspNetCore.Http;
1213
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
@@ -22,6 +23,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
2223
/// </summary>
2324
public class KestrelServerOptions
2425
{
26+
// internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged.
27+
internal static readonly Func<string, Encoding> DefaultRequestHeaderEncodingSelector = _ => null;
28+
29+
private Func<string, Encoding> _requestHeaderEncodingSelector = DefaultRequestHeaderEncodingSelector;
30+
2531
// The following two lists configure the endpoints that Kestrel should listen to. If both lists are empty, the "urls" config setting (e.g. UseUrls) is used.
2632
internal List<ListenOptions> CodeBackedListenOptions { get; } = new List<ListenOptions>();
2733
internal List<ListenOptions> ConfigurationBackedListenOptions { get; } = new List<ListenOptions>();
@@ -65,6 +71,24 @@ public class KestrelServerOptions
6571
/// </remarks>
6672
public bool DisableStringReuse { get; set; } = false;
6773

74+
/// <summary>
75+
/// Controls whether to return the AltSvcHeader from on an HTTP/2 or lower response for HTTP/3
76+
/// </summary>
77+
/// <remarks>
78+
/// Defaults to false.
79+
/// </remarks>
80+
public bool EnableAltSvc { get; set; } = false;
81+
82+
/// <summary>
83+
/// Gets or sets a callback that returns the <see cref="Encoding"/> to decode the value for the specified request header name,
84+
/// or <see langword="null"/> to use the default <see cref="UTF8Encoding"/>.
85+
/// </summary>
86+
public Func<string, Encoding> RequestHeaderEncodingSelector
87+
{
88+
get => _requestHeaderEncodingSelector;
89+
set => _requestHeaderEncodingSelector = value ?? throw new ArgumentNullException(nameof(value));
90+
}
91+
6892
/// <summary>
6993
/// Enables the Listen options callback to resolve and use services registered by the application during startup.
7094
/// Typically initialized by UseKestrel()"/>.
@@ -78,15 +102,10 @@ public class KestrelServerOptions
78102

79103
/// <summary>
80104
/// Provides a configuration source where endpoints will be loaded from on server start.
81-
/// The default is null.
105+
/// The default is <see langword="null"/>.
82106
/// </summary>
83107
public KestrelConfigurationLoader ConfigurationLoader { get; set; }
84108

85-
/// <summary>
86-
/// Controls whether to return the AltSvcHeader from on an HTTP/2 or lower response for HTTP/3
87-
/// </summary>
88-
public bool EnableAltSvc { get; set; } = false;
89-
90109
/// <summary>
91110
/// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs.
92111
/// </summary>
@@ -107,11 +126,6 @@ public class KestrelServerOptions
107126
/// </summary>
108127
internal bool IsDevCertLoaded { get; set; }
109128

110-
/// <summary>
111-
/// Treat request headers as Latin-1 or ISO/IEC 8859-1 instead of UTF-8.
112-
/// </summary>
113-
internal bool Latin1RequestHeaders { get; set; }
114-
115129
/// <summary>
116130
/// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace
117131
/// the prior action.
@@ -159,7 +173,7 @@ private void EnsureDefaultCert()
159173
if (DefaultCertificate == null && !IsDevCertLoaded)
160174
{
161175
IsDevCertLoaded = true; // Only try once
162-
var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
176+
var logger = ApplicationServices!.GetRequiredService<ILogger<KestrelServer>>();
163177
try
164178
{
165179
DefaultCertificate = CertificateManager.Instance.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true)
@@ -220,7 +234,7 @@ private void EnsureDefaultCert()
220234
/// </summary>
221235
/// <param name="config">The configuration section for Kestrel.</param>
222236
/// <param name="reloadOnChange">
223-
/// If <see langword="true" />, Kestrel will dynamically update endpoint bindings when configuration changes.
237+
/// If <see langword="true"/>, Kestrel will dynamically update endpoint bindings when configuration changes.
224238
/// This will only reload endpoints defined in the "Endpoints" section of your <paramref name="config"/>. Endpoints defined in code will not be reloaded.
225239
/// </param>
226240
/// <returns>A <see cref="KestrelConfigurationLoader"/> for further endpoint configuration.</returns>

0 commit comments

Comments
 (0)