Skip to content

Commit

Permalink
Use interned Header Names for known headers not in the pre-allocated …
Browse files Browse the repository at this point in the history
…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 <Tratcher@Outlook.com>

* Feedback

Co-authored-by: Chris Ross <Tratcher@Outlook.com>
  • Loading branch information
benaadams and Tratcher authored Mar 30, 2021
1 parent 9e150eb commit b00ae1b
Show file tree
Hide file tree
Showing 15 changed files with 360 additions and 150 deletions.
9 changes: 9 additions & 0 deletions src/Http/Headers/src/HeaderNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ public static class HeaderNames
/// <summary>Gets the <c>Last-Modified</c> HTTP header name.</summary>
public static readonly string LastModified = "Last-Modified";

/// <summary>Gets the <c>Link</c> HTTP header name.</summary>
public static readonly string Link = "Link";

/// <summary>Gets the <c>Location</c> HTTP header name.</summary>
public static readonly string Location = "Location";

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

/// <summary>Gets the <c>X-Content-Type-Options</c> HTTP header name.</summary>
public static readonly string XContentTypeOptions = "X-Content-Type-Options";

/// <summary>Gets the <c>X-Frame-Options</c> HTTP header name.</summary>
public static readonly string XFrameOptions = "X-Frame-Options";

/// <summary>Gets the <c>X-Requested-With</c> HTTP header name.</summary>
public static readonly string XRequestedWith = "X-Requested-With";

/// <summary>Gets the <c>X-XSS-Protection</c> HTTP header name.</summary>
public static readonly string XXssProtection = "X-XSS-Protection";
}
}
3 changes: 3 additions & 0 deletions src/Http/Headers/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
Microsoft.Net.Http.Headers.MediaTypeHeaderValue.MatchesMediaType(Microsoft.Extensions.Primitives.StringSegment otherMediaType) -> bool
Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> void
static readonly Microsoft.Net.Http.Headers.HeaderNames.Baggage -> string!
static readonly Microsoft.Net.Http.Headers.HeaderNames.Link -> string!
static readonly Microsoft.Net.Http.Headers.HeaderNames.ProxyConnection -> string!
static readonly Microsoft.Net.Http.Headers.HeaderNames.XContentTypeOptions -> string!
static readonly Microsoft.Net.Http.Headers.HeaderNames.XXssProtection -> string!
7 changes: 3 additions & 4 deletions src/Middleware/WebSockets/src/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ namespace Microsoft.AspNetCore.WebSockets
internal static class Constants
{
public static class Headers
{
public const string UpgradeWebSocket = "websocket";
public const string ConnectionUpgrade = "Upgrade";
public const string SupportedVersion = "13";
{
public readonly static string UpgradeWebSocket = "websocket";
public readonly static string SupportedVersion = "13";
}
}
}
22 changes: 18 additions & 4 deletions src/Middleware/WebSockets/src/HandshakeHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ internal static class HandshakeHelpers
};

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

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

foreach (var pair in headers)
foreach (var pair in interestingHeaders)
{
if (string.Equals(HeaderNames.Connection, pair.Key, StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(Constants.Headers.ConnectionUpgrade, pair.Value, StringComparison.OrdinalIgnoreCase))
if (string.Equals(HeaderNames.Upgrade, pair.Value, StringComparison.OrdinalIgnoreCase))
{
validConnection = true;
}
Expand All @@ -72,12 +72,26 @@ public static bool CheckSupportedWebSocketRequest(string method, List<KeyValuePa
}
}

// WebSockets are long lived; so if the header values are valid we switch them out for the interned versions.
if (validConnection && requestHeaders[HeaderNames.Connection].Count == 1)
{
requestHeaders[HeaderNames.Connection] = HeaderNames.Upgrade;
}
if (validUpgrade && requestHeaders[HeaderNames.Upgrade].Count == 1)
{
requestHeaders[HeaderNames.Upgrade] = Constants.Headers.UpgradeWebSocket;
}
if (validVersion && requestHeaders[HeaderNames.SecWebSocketVersion].Count == 1)
{
requestHeaders[HeaderNames.SecWebSocketVersion] = Constants.Headers.SupportedVersion;
}

return validConnection && validUpgrade && validVersion && validKey;
}

public static void GenerateResponseHeaders(string key, string? subProtocol, IHeaderDictionary headers)
{
headers[HeaderNames.Connection] = Constants.Headers.ConnectionUpgrade;
headers[HeaderNames.Connection] = HeaderNames.Upgrade;
headers[HeaderNames.Upgrade] = Constants.Headers.UpgradeWebSocket;
headers[HeaderNames.SecWebSocketAccept] = CreateResponseKey(key);
if (!string.IsNullOrWhiteSpace(subProtocol))
Expand Down
11 changes: 6 additions & 5 deletions src/Middleware/WebSockets/src/WebSocketMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,16 @@ public bool IsWebSocketRequest
}
else
{
var headers = new List<KeyValuePair<string, string>>();
foreach (string headerName in HandshakeHelpers.NeededHeaders)
var requestHeaders = _context.Request.Headers;
var interestingHeaders = new List<KeyValuePair<string, string>>();
foreach (var headerName in HandshakeHelpers.NeededHeaders)
{
foreach (var value in _context.Request.Headers.GetCommaSeparatedValues(headerName))
foreach (var value in requestHeaders.GetCommaSeparatedValues(headerName))
{
headers.Add(new KeyValuePair<string, string>(headerName, value));
interestingHeaders.Add(new KeyValuePair<string, string>(headerName, value));
}
}
_isWebSocketRequest = HandshakeHelpers.CheckSupportedWebSocketRequest(_context.Request.Method, headers);
_isWebSocketRequest = HandshakeHelpers.CheckSupportedWebSocketRequest(_context.Request.Method, interestingHeaders, requestHeaders);
}
}
return _isWebSocketRequest.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public static MessageBody For(

if (headers.HasConnection)
{
var connectionOptions = HttpHeaders.ParseConnection(headers.HeaderConnection);
var connectionOptions = HttpHeaders.ParseConnection(headers);

upgrade = (connectionOptions & ConnectionOptions.Upgrade) != 0;
keepAlive = keepAlive || (connectionOptions & ConnectionOptions.KeepAlive) != 0;
Expand Down
104 changes: 102 additions & 2 deletions src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,106 @@ internal enum KnownHeaderType
WWWAuthenticate,
}

internal partial class HttpHeaders
{
private readonly static HashSet<string> _internedHeaderNames = new HashSet<string>(93, StringComparer.OrdinalIgnoreCase)
{
HeaderNames.Accept,
HeaderNames.AcceptCharset,
HeaderNames.AcceptEncoding,
HeaderNames.AcceptLanguage,
HeaderNames.AcceptRanges,
HeaderNames.AccessControlAllowCredentials,
HeaderNames.AccessControlAllowHeaders,
HeaderNames.AccessControlAllowMethods,
HeaderNames.AccessControlAllowOrigin,
HeaderNames.AccessControlExposeHeaders,
HeaderNames.AccessControlMaxAge,
HeaderNames.AccessControlRequestHeaders,
HeaderNames.AccessControlRequestMethod,
HeaderNames.Age,
HeaderNames.Allow,
HeaderNames.AltSvc,
HeaderNames.Authority,
HeaderNames.Authorization,
HeaderNames.Baggage,
HeaderNames.CacheControl,
HeaderNames.Connection,
HeaderNames.ContentDisposition,
HeaderNames.ContentEncoding,
HeaderNames.ContentLanguage,
HeaderNames.ContentLength,
HeaderNames.ContentLocation,
HeaderNames.ContentMD5,
HeaderNames.ContentRange,
HeaderNames.ContentSecurityPolicy,
HeaderNames.ContentSecurityPolicyReportOnly,
HeaderNames.ContentType,
HeaderNames.CorrelationContext,
HeaderNames.Cookie,
HeaderNames.Date,
HeaderNames.DNT,
HeaderNames.ETag,
HeaderNames.Expires,
HeaderNames.Expect,
HeaderNames.From,
HeaderNames.GrpcAcceptEncoding,
HeaderNames.GrpcEncoding,
HeaderNames.GrpcMessage,
HeaderNames.GrpcStatus,
HeaderNames.GrpcTimeout,
HeaderNames.Host,
HeaderNames.KeepAlive,
HeaderNames.IfMatch,
HeaderNames.IfModifiedSince,
HeaderNames.IfNoneMatch,
HeaderNames.IfRange,
HeaderNames.IfUnmodifiedSince,
HeaderNames.LastModified,
HeaderNames.Link,
HeaderNames.Location,
HeaderNames.MaxForwards,
HeaderNames.Method,
HeaderNames.Origin,
HeaderNames.Path,
HeaderNames.Pragma,
HeaderNames.ProxyAuthenticate,
HeaderNames.ProxyAuthorization,
HeaderNames.ProxyConnection,
HeaderNames.Range,
HeaderNames.Referer,
HeaderNames.RetryAfter,
HeaderNames.RequestId,
HeaderNames.Scheme,
HeaderNames.SecWebSocketAccept,
HeaderNames.SecWebSocketKey,
HeaderNames.SecWebSocketProtocol,
HeaderNames.SecWebSocketVersion,
HeaderNames.Server,
HeaderNames.SetCookie,
HeaderNames.Status,
HeaderNames.StrictTransportSecurity,
HeaderNames.TE,
HeaderNames.Trailer,
HeaderNames.TransferEncoding,
HeaderNames.Translate,
HeaderNames.TraceParent,
HeaderNames.TraceState,
HeaderNames.Upgrade,
HeaderNames.UpgradeInsecureRequests,
HeaderNames.UserAgent,
HeaderNames.Vary,
HeaderNames.Via,
HeaderNames.Warning,
HeaderNames.WebSocketSubProtocols,
HeaderNames.WWWAuthenticate,
HeaderNames.XContentTypeOptions,
HeaderNames.XFrameOptions,
HeaderNames.XRequestedWith,
HeaderNames.XXssProtection,
};
}

internal partial class HttpRequestHeaders
{
private HeaderReferences _headers;
Expand All @@ -125,7 +225,7 @@ public StringValues HeaderCacheControl
_headers._CacheControl = value;
}
}
public StringValues HeaderConnection
public override StringValues HeaderConnection
{
get
{
Expand Down Expand Up @@ -8129,7 +8229,7 @@ public StringValues HeaderCacheControl
_headers._CacheControl = value;
}
}
public StringValues HeaderConnection
public override StringValues HeaderConnection
{
get
{
Expand Down
61 changes: 57 additions & 4 deletions src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
internal abstract class HttpHeaders : IHeaderDictionary
internal abstract partial class HttpHeaders : IHeaderDictionary
{
protected long _bits = 0;
protected long? _contentLength;
protected bool _isReadOnly;
protected Dictionary<string, StringValues>? MaybeUnknown;
protected Dictionary<string, StringValues> Unknown => MaybeUnknown ?? (MaybeUnknown = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase));
protected Dictionary<string, StringValues> Unknown => MaybeUnknown ??= new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);

public long? ContentLength
{
Expand All @@ -40,6 +41,8 @@ public long? ContentLength
}
}

public abstract StringValues HeaderConnection { get; set; }

StringValues IHeaderDictionary.this[string key]
{
get
Expand Down Expand Up @@ -126,6 +129,18 @@ public void Reset()
ClearFast();
}

protected static string GetInternedHeaderName(string name)
{
// Some headers can be very long lived; for example those on a WebSocket connection
// so we exchange these for the preallocated strings predefined in HeaderNames
if (_internedHeaderNames.TryGetValue(name, out var internedName))
{
return internedName;
}

return name;
}

[MethodImpl(MethodImplOptions.NoInlining)]
protected static StringValues AppendValue(StringValues existing, string append)
{
Expand Down Expand Up @@ -276,7 +291,12 @@ public static void ValidateHeaderNameCharacters(string headerCharacters)
}
}

public static ConnectionOptions ParseConnection(StringValues connection)
private readonly static string KeepAlive = "keep-alive";
private readonly static StringValues ConnectionValueKeepAlive = KeepAlive;
private readonly static StringValues ConnectionValueClose = "close";
private readonly static StringValues ConnectionValueUpgrade = HeaderNames.Upgrade;

public static ConnectionOptions ParseConnection(HttpHeaders headers)
{
// Keep-alive
const ulong lowerCaseKeep = 0x0000_0020_0020_0020; // Don't lowercase hyphen
Expand All @@ -289,9 +309,27 @@ public static ConnectionOptions ParseConnection(StringValues connection)
// Close
const ulong closeEnd = 0x0065_0073_006f_006c; // 4 chars "lose"

var connection = headers.HeaderConnection;
var connectionCount = connection.Count;
if (connectionCount == 0)
{
return ConnectionOptions.None;
}

var connectionOptions = ConnectionOptions.None;

var connectionCount = connection.Count;
if (connectionCount == 1)
{
// "keep-alive" is the only value that will be repeated over
// many requests on the same connection; on the first request
// we will have switched it for the readonly static value;
// so we can ptentially short-circuit parsing and use ReferenceEquals.
if (ReferenceEquals(connection.ToString(), KeepAlive))
{
return ConnectionOptions.KeepAlive;
}
}

for (var i = 0; i < connectionCount; i++)
{
var value = connection[i].AsSpan();
Expand Down Expand Up @@ -420,6 +458,21 @@ public static ConnectionOptions ParseConnection(StringValues connection)
}
}

// If Connection is a single value, switch it for the interned value
// in case the connection is long lived
if (connectionOptions == ConnectionOptions.Upgrade)
{
headers.HeaderConnection = ConnectionValueUpgrade;
}
else if (connectionOptions == ConnectionOptions.KeepAlive)
{
headers.HeaderConnection = ConnectionValueKeepAlive;
}
else if (connectionOptions == ConnectionOptions.Close)
{
headers.HeaderConnection = ConnectionValueClose;
}

return connectionOptions;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ async Task<Stream> IHttpUpgradeFeature.UpgradeAsync()

StatusCode = StatusCodes.Status101SwitchingProtocols;
ReasonPhrase = "Switching Protocols";
ResponseHeaders[HeaderNames.Connection] = "Upgrade";
ResponseHeaders[HeaderNames.Connection] = HeaderNames.Upgrade;

await FlushAsync();

Expand Down
2 changes: 1 addition & 1 deletion src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,7 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted)

if (_keepAlive &&
hasConnection &&
(HttpHeaders.ParseConnection(responseHeaders.HeaderConnection) & ConnectionOptions.KeepAlive) == 0)
(HttpHeaders.ParseConnection(responseHeaders) & ConnectionOptions.KeepAlive) == 0)
{
_keepAlive = false;
}
Expand Down
Loading

0 comments on commit b00ae1b

Please sign in to comment.