Skip to content

Commit f81da62

Browse files
committed
Add custom request header decoder API to Kestrel
1 parent c2bfbf5 commit f81da62

File tree

14 files changed

+270
-69
lines changed

14 files changed

+270
-69
lines changed

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

Lines changed: 67 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.GetRequestHeaderEncodingSelector();
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: 33 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,12 +89,36 @@ 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));
9193
}
9294

9395
_contentLength = parsed;
9496
}
9597

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));
116+
}
117+
118+
_contentLength = parsed;
119+
}
120+
121+
96122
[MethodImpl(MethodImplOptions.NoInlining)]
97123
private void SetValueUnknown(string key, StringValues value)
98124
{
@@ -108,11 +134,10 @@ private bool AddValueUnknown(string key, StringValues value)
108134
}
109135

110136
[MethodImpl(MethodImplOptions.NoInlining)]
111-
private unsafe void AppendUnknownHeaders(ReadOnlySpan<byte> name, string valueString)
137+
private unsafe void AppendUnknownHeaders(string name, string valueString)
112138
{
113-
string key = name.GetHeaderName();
114-
Unknown.TryGetValue(key, out var existing);
115-
Unknown[key] = AppendValue(existing, valueString);
139+
Unknown.TryGetValue(name, out var existing);
140+
Unknown[name] = AppendValue(existing, valueString);
116141
}
117142

118143
public Enumerator GetEnumerator()

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

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Runtime.InteropServices;
1010
using System.Text;
1111
using Microsoft.AspNetCore.Http;
12+
using Microsoft.AspNetCore.Http.Features;
1213
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
1314

1415
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
@@ -27,7 +28,6 @@ internal static partial class HttpUtilities
2728
private const ulong _http10VersionLong = 3471766442030158920; // GetAsciiStringAsLong("HTTP/1.0"); const results in better codegen
2829
private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
2930

30-
private static readonly UTF8EncodingSealed HeaderValueEncoding = 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, KestrelServerOptions.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(KestrelServerOptions.DefaultRequestHeaderEncoding);
144+
}
145+
146+
var encoding = encodingSelector(name);
147+
148+
if (encoding is null)
149+
{
150+
return span.GetAsciiOrUTF8StringNonNullCharacters(KestrelServerOptions.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)
163+
{
164+
throw new InvalidOperationException();
165+
}
166+
}
144167

145168
public static string GetAsciiStringEscaped(this ReadOnlySpan<byte> span, int maxChars)
146169
{
@@ -529,13 +552,5 @@ private static bool IsHex(char ch)
529552
// Check if less than 6 representing chars 'a' - 'f'
530553
|| (uint)((ch | 32) - 'a') < 6u;
531554
}
532-
533-
// Allow for de-virtualization (see https://github.com/dotnet/coreclr/pull/9230)
534-
private sealed class UTF8EncodingSealed : UTF8Encoding
535-
{
536-
public UTF8EncodingSealed() : base(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true) { }
537-
538-
public override byte[] GetPreamble() => Array.Empty<byte>();
539-
}
540555
}
541556
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Text;
6+
7+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
8+
{
9+
// Allow for de-virtualization (see https://github.com/dotnet/coreclr/pull/9230)
10+
internal sealed class UTF8EncodingSealed : UTF8Encoding
11+
{
12+
public UTF8EncodingSealed() : base(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true) { }
13+
14+
public override byte[] GetPreamble() => Array.Empty<byte>();
15+
}
16+
}

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
using System.Collections.Generic;
66
using System.IO.Pipelines;
77
using System.Linq;
8+
using System.Text;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using Microsoft.AspNetCore.Connections;
1112
using Microsoft.AspNetCore.Hosting.Server;
1213
using Microsoft.AspNetCore.Hosting.Server.Features;
14+
using Microsoft.AspNetCore.Http;
1315
using Microsoft.AspNetCore.Http.Features;
1416
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
1517
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
@@ -35,12 +37,19 @@ public class KestrelServer : IServer
3537

3638
private IDisposable _configChangedRegistration;
3739

38-
public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, ILoggerFactory loggerFactory)
40+
public KestrelServer(
41+
IOptions<KestrelServerOptions> options,
42+
IEnumerable<IConnectionListenerFactory> transportFactories,
43+
ILoggerFactory loggerFactory)
3944
: this(transportFactories, null, CreateServiceContext(options, loggerFactory))
4045
{
4146
}
4247

43-
public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories, ILoggerFactory loggerFactory)
48+
public KestrelServer(
49+
IOptions<KestrelServerOptions> options,
50+
IEnumerable<IConnectionListenerFactory> transportFactories,
51+
IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
52+
ILoggerFactory loggerFactory)
4453
: this(transportFactories, multiplexedFactories, CreateServiceContext(options, loggerFactory))
4554
{
4655
}
@@ -52,7 +61,10 @@ internal KestrelServer(IEnumerable<IConnectionListenerFactory> transportFactorie
5261
}
5362

5463
// For testing
55-
internal KestrelServer(IEnumerable<IConnectionListenerFactory> transportFactories, IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories, ServiceContext serviceContext)
64+
internal KestrelServer(
65+
IEnumerable<IConnectionListenerFactory> transportFactories,
66+
IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
67+
ServiceContext serviceContext)
5668
{
5769
if (transportFactories == null)
5870
{
@@ -362,6 +374,11 @@ private void ValidateOptions()
362374
throw new InvalidOperationException(
363375
CoreStrings.FormatMaxRequestBufferSmallerThanRequestHeaderBuffer(Options.Limits.MaxRequestBufferSize.Value, Options.Limits.MaxRequestHeadersTotalSize));
364376
}
377+
378+
if (Options.RequestHeaderEncodingSelector is null)
379+
{
380+
throw new InvalidOperationException($"{nameof(KestrelServerOptions)}.{nameof(KestrelServerOptions.RequestHeaderEncodingSelector)} must not be null.");
381+
}
365382
}
366383

367384
private static ConnectionDelegate EnforceConnectionLimit(ConnectionDelegate innerDelegate, long? connectionLimit, IKestrelTrace trace)

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

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
#nullable enable
5+
46
using System;
57
using System.Collections.Generic;
68
using System.IO;
79
using System.Linq;
810
using System.Net;
911
using System.Security.Cryptography.X509Certificates;
12+
using System.Text;
1013
using Microsoft.AspNetCore.Certificates.Generation;
1114
using Microsoft.AspNetCore.Http;
1215
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
16+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
1317
using Microsoft.AspNetCore.Server.Kestrel.Https;
1418
using Microsoft.Extensions.Configuration;
1519
using Microsoft.Extensions.DependencyInjection;
@@ -22,6 +26,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
2226
/// </summary>
2327
public class KestrelServerOptions
2428
{
29+
// Internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged.
30+
internal static readonly UTF8EncodingSealed DefaultRequestHeaderEncoding = new UTF8EncodingSealed();
31+
internal static readonly Func<string, Encoding> DefaultRequestHeaderEncodingSelector = _ => DefaultRequestHeaderEncoding;
32+
internal static readonly Func<string, Encoding> DefaultLatin1RequestHeaderEncodingSelector = _ => Encoding.Latin1;
33+
2534
// 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.
2635
internal List<ListenOptions> CodeBackedListenOptions { get; } = new List<ListenOptions>();
2736
internal List<ListenOptions> ConfigurationBackedListenOptions { get; } = new List<ListenOptions>();
@@ -65,11 +74,27 @@ public class KestrelServerOptions
6574
/// </remarks>
6675
public bool DisableStringReuse { get; set; } = false;
6776

77+
/// <summary>
78+
/// Controls whether to return the AltSvcHeader from on an HTTP/2 or lower response for HTTP/3
79+
/// </summary>
80+
/// <remarks>
81+
/// Defaults to false.
82+
/// </remarks>
83+
public bool EnableAltSvc { get; set; } = false;
84+
85+
/// <summary>
86+
/// Gets or sets a callback that returns the <see cref="Encoding"/> to decode the value for the specified request header name.
87+
/// </summary>
88+
/// <remarks>
89+
/// Defaults to returning a <see cref="UTF8Encoding"/> for all headers.
90+
/// </remarks>
91+
public Func<string, Encoding?> RequestHeaderEncodingSelector { get; set; } = DefaultRequestHeaderEncodingSelector;
92+
6893
/// <summary>
6994
/// Enables the Listen options callback to resolve and use services registered by the application during startup.
7095
/// Typically initialized by UseKestrel()"/>.
7196
/// </summary>
72-
public IServiceProvider ApplicationServices { get; set; }
97+
public IServiceProvider? ApplicationServices { get; set; }
7398

7499
/// <summary>
75100
/// Provides access to request limit options.
@@ -80,12 +105,7 @@ public class KestrelServerOptions
80105
/// Provides a configuration source where endpoints will be loaded from on server start.
81106
/// The default is null.
82107
/// </summary>
83-
public KestrelConfigurationLoader ConfigurationLoader { get; set; }
84-
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;
108+
public KestrelConfigurationLoader? ConfigurationLoader { get; set; }
89109

90110
/// <summary>
91111
/// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs.
@@ -100,7 +120,7 @@ public class KestrelServerOptions
100120
/// <summary>
101121
/// The default server certificate for https endpoints. This is applied lazily after HttpsDefaults and user options.
102122
/// </summary>
103-
internal X509Certificate2 DefaultCertificate { get; set; }
123+
internal X509Certificate2? DefaultCertificate { get; set; }
104124

105125
/// <summary>
106126
/// Has the default dev certificate load been attempted?
@@ -121,9 +141,19 @@ public void ConfigureEndpointDefaults(Action<ListenOptions> configureOptions)
121141
EndpointDefaults = configureOptions ?? throw new ArgumentNullException(nameof(configureOptions));
122142
}
123143

144+
internal Func<string, Encoding?> GetRequestHeaderEncodingSelector()
145+
{
146+
if (ReferenceEquals(RequestHeaderEncodingSelector, DefaultRequestHeaderEncodingSelector) && Latin1RequestHeaders)
147+
{
148+
return DefaultLatin1RequestHeaderEncodingSelector;
149+
}
150+
151+
return RequestHeaderEncodingSelector;
152+
}
153+
124154
internal void ApplyEndpointDefaults(ListenOptions listenOptions)
125155
{
126-
listenOptions.KestrelServerOptions = this;
156+
listenOptions.KestrelServerOptions = this;
127157
ConfigurationLoader?.ApplyConfigurationDefaults(listenOptions);
128158
EndpointDefaults(listenOptions);
129159
}
@@ -159,7 +189,7 @@ private void EnsureDefaultCert()
159189
if (DefaultCertificate == null && !IsDevCertLoaded)
160190
{
161191
IsDevCertLoaded = true; // Only try once
162-
var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
192+
var logger = ApplicationServices!.GetRequiredService<ILogger<KestrelServer>>();
163193
try
164194
{
165195
DefaultCertificate = CertificateManager.Instance.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true)
@@ -220,10 +250,11 @@ private void EnsureDefaultCert()
220250
/// </summary>
221251
/// <param name="config">The configuration section for Kestrel.</param>
222252
/// <param name="reloadOnChange">
223-
/// If <see langword="true" />, Kestrel will dynamically update endpoint bindings when configuration changes.
253+
/// If <see langword="true"/>, Kestrel will dynamically update endpoint bindings when configuration changes.
224254
/// This will only reload endpoints defined in the "Endpoints" section of your <paramref name="config"/>. Endpoints defined in code will not be reloaded.
225255
/// </param>
226256
/// <returns>A <see cref="KestrelConfigurationLoader"/> for further endpoint configuration.</returns>
257+
227258
public KestrelConfigurationLoader Configure(IConfiguration config, bool reloadOnChange)
228259
{
229260
var loader = new KestrelConfigurationLoader(this, config, reloadOnChange);

0 commit comments

Comments
 (0)