Skip to content

Commit b00ae1b

Browse files
benaadamsTratcher
andauthored
Use interned Header Names for known headers not in the pre-allocated block (#31311)
* Use interned headernames for known headers not in the preallocated block * Switch Connection header values for interned values * Use interned strings for websockets * Keep baggage, tracestate, and traceparent with previous casing * Update src/Servers/Kestrel/shared/KnownHeaders.cs Co-authored-by: Chris Ross <[email protected]> * Feedback Co-authored-by: Chris Ross <[email protected]>
1 parent 9e150eb commit b00ae1b

15 files changed

+360
-150
lines changed

src/Http/Headers/src/HeaderNames.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ public static class HeaderNames
166166
/// <summary>Gets the <c>Last-Modified</c> HTTP header name.</summary>
167167
public static readonly string LastModified = "Last-Modified";
168168

169+
/// <summary>Gets the <c>Link</c> HTTP header name.</summary>
170+
public static readonly string Link = "Link";
171+
169172
/// <summary>Gets the <c>Location</c> HTTP header name.</summary>
170173
public static readonly string Location = "Location";
171174

@@ -274,10 +277,16 @@ public static class HeaderNames
274277
/// <summary>Gets the <c>WWW-Authenticate</c> HTTP header name.</summary>
275278
public static readonly string WWWAuthenticate = "WWW-Authenticate";
276279

280+
/// <summary>Gets the <c>X-Content-Type-Options</c> HTTP header name.</summary>
281+
public static readonly string XContentTypeOptions = "X-Content-Type-Options";
282+
277283
/// <summary>Gets the <c>X-Frame-Options</c> HTTP header name.</summary>
278284
public static readonly string XFrameOptions = "X-Frame-Options";
279285

280286
/// <summary>Gets the <c>X-Requested-With</c> HTTP header name.</summary>
281287
public static readonly string XRequestedWith = "X-Requested-With";
288+
289+
/// <summary>Gets the <c>X-XSS-Protection</c> HTTP header name.</summary>
290+
public static readonly string XXssProtection = "X-XSS-Protection";
282291
}
283292
}

src/Http/Headers/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@
33
Microsoft.Net.Http.Headers.MediaTypeHeaderValue.MatchesMediaType(Microsoft.Extensions.Primitives.StringSegment otherMediaType) -> bool
44
Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> void
55
static readonly Microsoft.Net.Http.Headers.HeaderNames.Baggage -> string!
6+
static readonly Microsoft.Net.Http.Headers.HeaderNames.Link -> string!
67
static readonly Microsoft.Net.Http.Headers.HeaderNames.ProxyConnection -> string!
8+
static readonly Microsoft.Net.Http.Headers.HeaderNames.XContentTypeOptions -> string!
9+
static readonly Microsoft.Net.Http.Headers.HeaderNames.XXssProtection -> string!

src/Middleware/WebSockets/src/Constants.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ namespace Microsoft.AspNetCore.WebSockets
66
internal static class Constants
77
{
88
public static class Headers
9-
{
10-
public const string UpgradeWebSocket = "websocket";
11-
public const string ConnectionUpgrade = "Upgrade";
12-
public const string SupportedVersion = "13";
9+
{
10+
public readonly static string UpgradeWebSocket = "websocket";
11+
public readonly static string SupportedVersion = "13";
1312
}
1413
}
1514
}

src/Middleware/WebSockets/src/HandshakeHelpers.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ internal static class HandshakeHelpers
3434
};
3535

3636
// Verify Method, Upgrade, Connection, version, key, etc..
37-
public static bool CheckSupportedWebSocketRequest(string method, List<KeyValuePair<string, string>> headers)
37+
public static bool CheckSupportedWebSocketRequest(string method, List<KeyValuePair<string, string>> interestingHeaders, IHeaderDictionary requestHeaders)
3838
{
3939
bool validUpgrade = false, validConnection = false, validKey = false, validVersion = false;
4040

@@ -43,11 +43,11 @@ public static bool CheckSupportedWebSocketRequest(string method, List<KeyValuePa
4343
return false;
4444
}
4545

46-
foreach (var pair in headers)
46+
foreach (var pair in interestingHeaders)
4747
{
4848
if (string.Equals(HeaderNames.Connection, pair.Key, StringComparison.OrdinalIgnoreCase))
4949
{
50-
if (string.Equals(Constants.Headers.ConnectionUpgrade, pair.Value, StringComparison.OrdinalIgnoreCase))
50+
if (string.Equals(HeaderNames.Upgrade, pair.Value, StringComparison.OrdinalIgnoreCase))
5151
{
5252
validConnection = true;
5353
}
@@ -72,12 +72,26 @@ public static bool CheckSupportedWebSocketRequest(string method, List<KeyValuePa
7272
}
7373
}
7474

75+
// WebSockets are long lived; so if the header values are valid we switch them out for the interned versions.
76+
if (validConnection && requestHeaders[HeaderNames.Connection].Count == 1)
77+
{
78+
requestHeaders[HeaderNames.Connection] = HeaderNames.Upgrade;
79+
}
80+
if (validUpgrade && requestHeaders[HeaderNames.Upgrade].Count == 1)
81+
{
82+
requestHeaders[HeaderNames.Upgrade] = Constants.Headers.UpgradeWebSocket;
83+
}
84+
if (validVersion && requestHeaders[HeaderNames.SecWebSocketVersion].Count == 1)
85+
{
86+
requestHeaders[HeaderNames.SecWebSocketVersion] = Constants.Headers.SupportedVersion;
87+
}
88+
7589
return validConnection && validUpgrade && validVersion && validKey;
7690
}
7791

7892
public static void GenerateResponseHeaders(string key, string? subProtocol, IHeaderDictionary headers)
7993
{
80-
headers[HeaderNames.Connection] = Constants.Headers.ConnectionUpgrade;
94+
headers[HeaderNames.Connection] = HeaderNames.Upgrade;
8195
headers[HeaderNames.Upgrade] = Constants.Headers.UpgradeWebSocket;
8296
headers[HeaderNames.SecWebSocketAccept] = CreateResponseKey(key);
8397
if (!string.IsNullOrWhiteSpace(subProtocol))

src/Middleware/WebSockets/src/WebSocketMiddleware.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,16 @@ public bool IsWebSocketRequest
118118
}
119119
else
120120
{
121-
var headers = new List<KeyValuePair<string, string>>();
122-
foreach (string headerName in HandshakeHelpers.NeededHeaders)
121+
var requestHeaders = _context.Request.Headers;
122+
var interestingHeaders = new List<KeyValuePair<string, string>>();
123+
foreach (var headerName in HandshakeHelpers.NeededHeaders)
123124
{
124-
foreach (var value in _context.Request.Headers.GetCommaSeparatedValues(headerName))
125+
foreach (var value in requestHeaders.GetCommaSeparatedValues(headerName))
125126
{
126-
headers.Add(new KeyValuePair<string, string>(headerName, value));
127+
interestingHeaders.Add(new KeyValuePair<string, string>(headerName, value));
127128
}
128129
}
129-
_isWebSocketRequest = HandshakeHelpers.CheckSupportedWebSocketRequest(_context.Request.Method, headers);
130+
_isWebSocketRequest = HandshakeHelpers.CheckSupportedWebSocketRequest(_context.Request.Method, interestingHeaders, requestHeaders);
130131
}
131132
}
132133
return _isWebSocketRequest.Value;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public static MessageBody For(
128128

129129
if (headers.HasConnection)
130130
{
131-
var connectionOptions = HttpHeaders.ParseConnection(headers.HeaderConnection);
131+
var connectionOptions = HttpHeaders.ParseConnection(headers);
132132

133133
upgrade = (connectionOptions & ConnectionOptions.Upgrade) != 0;
134134
keepAlive = keepAlive || (connectionOptions & ConnectionOptions.KeepAlive) != 0;

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

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,106 @@ internal enum KnownHeaderType
9999
WWWAuthenticate,
100100
}
101101

102+
internal partial class HttpHeaders
103+
{
104+
private readonly static HashSet<string> _internedHeaderNames = new HashSet<string>(93, StringComparer.OrdinalIgnoreCase)
105+
{
106+
HeaderNames.Accept,
107+
HeaderNames.AcceptCharset,
108+
HeaderNames.AcceptEncoding,
109+
HeaderNames.AcceptLanguage,
110+
HeaderNames.AcceptRanges,
111+
HeaderNames.AccessControlAllowCredentials,
112+
HeaderNames.AccessControlAllowHeaders,
113+
HeaderNames.AccessControlAllowMethods,
114+
HeaderNames.AccessControlAllowOrigin,
115+
HeaderNames.AccessControlExposeHeaders,
116+
HeaderNames.AccessControlMaxAge,
117+
HeaderNames.AccessControlRequestHeaders,
118+
HeaderNames.AccessControlRequestMethod,
119+
HeaderNames.Age,
120+
HeaderNames.Allow,
121+
HeaderNames.AltSvc,
122+
HeaderNames.Authority,
123+
HeaderNames.Authorization,
124+
HeaderNames.Baggage,
125+
HeaderNames.CacheControl,
126+
HeaderNames.Connection,
127+
HeaderNames.ContentDisposition,
128+
HeaderNames.ContentEncoding,
129+
HeaderNames.ContentLanguage,
130+
HeaderNames.ContentLength,
131+
HeaderNames.ContentLocation,
132+
HeaderNames.ContentMD5,
133+
HeaderNames.ContentRange,
134+
HeaderNames.ContentSecurityPolicy,
135+
HeaderNames.ContentSecurityPolicyReportOnly,
136+
HeaderNames.ContentType,
137+
HeaderNames.CorrelationContext,
138+
HeaderNames.Cookie,
139+
HeaderNames.Date,
140+
HeaderNames.DNT,
141+
HeaderNames.ETag,
142+
HeaderNames.Expires,
143+
HeaderNames.Expect,
144+
HeaderNames.From,
145+
HeaderNames.GrpcAcceptEncoding,
146+
HeaderNames.GrpcEncoding,
147+
HeaderNames.GrpcMessage,
148+
HeaderNames.GrpcStatus,
149+
HeaderNames.GrpcTimeout,
150+
HeaderNames.Host,
151+
HeaderNames.KeepAlive,
152+
HeaderNames.IfMatch,
153+
HeaderNames.IfModifiedSince,
154+
HeaderNames.IfNoneMatch,
155+
HeaderNames.IfRange,
156+
HeaderNames.IfUnmodifiedSince,
157+
HeaderNames.LastModified,
158+
HeaderNames.Link,
159+
HeaderNames.Location,
160+
HeaderNames.MaxForwards,
161+
HeaderNames.Method,
162+
HeaderNames.Origin,
163+
HeaderNames.Path,
164+
HeaderNames.Pragma,
165+
HeaderNames.ProxyAuthenticate,
166+
HeaderNames.ProxyAuthorization,
167+
HeaderNames.ProxyConnection,
168+
HeaderNames.Range,
169+
HeaderNames.Referer,
170+
HeaderNames.RetryAfter,
171+
HeaderNames.RequestId,
172+
HeaderNames.Scheme,
173+
HeaderNames.SecWebSocketAccept,
174+
HeaderNames.SecWebSocketKey,
175+
HeaderNames.SecWebSocketProtocol,
176+
HeaderNames.SecWebSocketVersion,
177+
HeaderNames.Server,
178+
HeaderNames.SetCookie,
179+
HeaderNames.Status,
180+
HeaderNames.StrictTransportSecurity,
181+
HeaderNames.TE,
182+
HeaderNames.Trailer,
183+
HeaderNames.TransferEncoding,
184+
HeaderNames.Translate,
185+
HeaderNames.TraceParent,
186+
HeaderNames.TraceState,
187+
HeaderNames.Upgrade,
188+
HeaderNames.UpgradeInsecureRequests,
189+
HeaderNames.UserAgent,
190+
HeaderNames.Vary,
191+
HeaderNames.Via,
192+
HeaderNames.Warning,
193+
HeaderNames.WebSocketSubProtocols,
194+
HeaderNames.WWWAuthenticate,
195+
HeaderNames.XContentTypeOptions,
196+
HeaderNames.XFrameOptions,
197+
HeaderNames.XRequestedWith,
198+
HeaderNames.XXssProtection,
199+
};
200+
}
201+
102202
internal partial class HttpRequestHeaders
103203
{
104204
private HeaderReferences _headers;
@@ -125,7 +225,7 @@ public StringValues HeaderCacheControl
125225
_headers._CacheControl = value;
126226
}
127227
}
128-
public StringValues HeaderConnection
228+
public override StringValues HeaderConnection
129229
{
130230
get
131231
{
@@ -8129,7 +8229,7 @@ public StringValues HeaderCacheControl
81298229
_headers._CacheControl = value;
81308230
}
81318231
}
8132-
public StringValues HeaderConnection
8232+
public override StringValues HeaderConnection
81338233
{
81348234
get
81358235
{

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

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@
1212
using Microsoft.AspNetCore.Http;
1313
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
1414
using Microsoft.Extensions.Primitives;
15+
using Microsoft.Net.Http.Headers;
1516

1617
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
1718
{
18-
internal abstract class HttpHeaders : IHeaderDictionary
19+
internal abstract partial class HttpHeaders : IHeaderDictionary
1920
{
2021
protected long _bits = 0;
2122
protected long? _contentLength;
2223
protected bool _isReadOnly;
2324
protected Dictionary<string, StringValues>? MaybeUnknown;
24-
protected Dictionary<string, StringValues> Unknown => MaybeUnknown ?? (MaybeUnknown = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase));
25+
protected Dictionary<string, StringValues> Unknown => MaybeUnknown ??= new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
2526

2627
public long? ContentLength
2728
{
@@ -40,6 +41,8 @@ public long? ContentLength
4041
}
4142
}
4243

44+
public abstract StringValues HeaderConnection { get; set; }
45+
4346
StringValues IHeaderDictionary.this[string key]
4447
{
4548
get
@@ -126,6 +129,18 @@ public void Reset()
126129
ClearFast();
127130
}
128131

132+
protected static string GetInternedHeaderName(string name)
133+
{
134+
// Some headers can be very long lived; for example those on a WebSocket connection
135+
// so we exchange these for the preallocated strings predefined in HeaderNames
136+
if (_internedHeaderNames.TryGetValue(name, out var internedName))
137+
{
138+
return internedName;
139+
}
140+
141+
return name;
142+
}
143+
129144
[MethodImpl(MethodImplOptions.NoInlining)]
130145
protected static StringValues AppendValue(StringValues existing, string append)
131146
{
@@ -276,7 +291,12 @@ public static void ValidateHeaderNameCharacters(string headerCharacters)
276291
}
277292
}
278293

279-
public static ConnectionOptions ParseConnection(StringValues connection)
294+
private readonly static string KeepAlive = "keep-alive";
295+
private readonly static StringValues ConnectionValueKeepAlive = KeepAlive;
296+
private readonly static StringValues ConnectionValueClose = "close";
297+
private readonly static StringValues ConnectionValueUpgrade = HeaderNames.Upgrade;
298+
299+
public static ConnectionOptions ParseConnection(HttpHeaders headers)
280300
{
281301
// Keep-alive
282302
const ulong lowerCaseKeep = 0x0000_0020_0020_0020; // Don't lowercase hyphen
@@ -289,9 +309,27 @@ public static ConnectionOptions ParseConnection(StringValues connection)
289309
// Close
290310
const ulong closeEnd = 0x0065_0073_006f_006c; // 4 chars "lose"
291311

312+
var connection = headers.HeaderConnection;
313+
var connectionCount = connection.Count;
314+
if (connectionCount == 0)
315+
{
316+
return ConnectionOptions.None;
317+
}
318+
292319
var connectionOptions = ConnectionOptions.None;
293320

294-
var connectionCount = connection.Count;
321+
if (connectionCount == 1)
322+
{
323+
// "keep-alive" is the only value that will be repeated over
324+
// many requests on the same connection; on the first request
325+
// we will have switched it for the readonly static value;
326+
// so we can ptentially short-circuit parsing and use ReferenceEquals.
327+
if (ReferenceEquals(connection.ToString(), KeepAlive))
328+
{
329+
return ConnectionOptions.KeepAlive;
330+
}
331+
}
332+
295333
for (var i = 0; i < connectionCount; i++)
296334
{
297335
var value = connection[i].AsSpan();
@@ -420,6 +458,21 @@ public static ConnectionOptions ParseConnection(StringValues connection)
420458
}
421459
}
422460

461+
// If Connection is a single value, switch it for the interned value
462+
// in case the connection is long lived
463+
if (connectionOptions == ConnectionOptions.Upgrade)
464+
{
465+
headers.HeaderConnection = ConnectionValueUpgrade;
466+
}
467+
else if (connectionOptions == ConnectionOptions.KeepAlive)
468+
{
469+
headers.HeaderConnection = ConnectionValueKeepAlive;
470+
}
471+
else if (connectionOptions == ConnectionOptions.Close)
472+
{
473+
headers.HeaderConnection = ConnectionValueClose;
474+
}
475+
423476
return connectionOptions;
424477
}
425478

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ async Task<Stream> IHttpUpgradeFeature.UpgradeAsync()
276276

277277
StatusCode = StatusCodes.Status101SwitchingProtocols;
278278
ReasonPhrase = "Switching Protocols";
279-
ResponseHeaders[HeaderNames.Connection] = "Upgrade";
279+
ResponseHeaders[HeaderNames.Connection] = HeaderNames.Upgrade;
280280

281281
await FlushAsync();
282282

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,7 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted)
11221122

11231123
if (_keepAlive &&
11241124
hasConnection &&
1125-
(HttpHeaders.ParseConnection(responseHeaders.HeaderConnection) & ConnectionOptions.KeepAlive) == 0)
1125+
(HttpHeaders.ParseConnection(responseHeaders) & ConnectionOptions.KeepAlive) == 0)
11261126
{
11271127
_keepAlive = false;
11281128
}

0 commit comments

Comments
 (0)