diff --git a/src/Http/WebUtilities/src/FormPipeReader.cs b/src/Http/WebUtilities/src/FormPipeReader.cs index 60f337757b..e6372c29e4 100644 --- a/src/Http/WebUtilities/src/FormPipeReader.cs +++ b/src/Http/WebUtilities/src/FormPipeReader.cs @@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.WebUtilities private const int DefaultValueLengthLimit = 1024 * 1024 * 4; // Used for UTF8/ASCII (precalculated for fast path) + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static private static ReadOnlySpan UTF8EqualEncoded => new byte[] { (byte)'=' }; private static ReadOnlySpan UTF8AndEncoded => new byte[] { (byte)'&' }; diff --git a/src/Middleware/WebSockets/src/HandshakeHelpers.cs b/src/Middleware/WebSockets/src/HandshakeHelpers.cs index 58a176348e..64e6c435da 100644 --- a/src/Middleware/WebSockets/src/HandshakeHelpers.cs +++ b/src/Middleware/WebSockets/src/HandshakeHelpers.cs @@ -24,7 +24,8 @@ namespace Microsoft.AspNetCore.WebSockets }; // "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - private static ReadOnlySpan _encodedWebSocketKey => new byte[] + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static + private static ReadOnlySpan EncodedWebSocketKey => new byte[] { (byte)'2', (byte)'5', (byte)'8', (byte)'E', (byte)'A', (byte)'F', (byte)'A', (byte)'5', (byte)'-', (byte)'E', (byte)'9', (byte)'1', (byte)'4', (byte)'-', (byte)'4', (byte)'7', (byte)'D', (byte)'A', @@ -115,7 +116,7 @@ namespace Microsoft.AspNetCore.WebSockets // so this can be hardcoded to 60 bytes for the requestKey + static websocket string Span mergedBytes = stackalloc byte[60]; Encoding.UTF8.GetBytes(requestKey, mergedBytes); - _encodedWebSocketKey.CopyTo(mergedBytes.Slice(24)); + EncodedWebSocketKey.CopyTo(mergedBytes.Slice(24)); Span hashedBytes = stackalloc byte[20]; var success = algorithm.TryComputeHash(mergedBytes, hashedBytes, out var written); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs index 75186e8ad6..8d64e7b762 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs @@ -6,12 +6,14 @@ using System.Buffers; using System.IO.Pipelines; using System.Text; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal static class ChunkWriter { - private static readonly byte[] _hex = Encoding.ASCII.GetBytes("0123456789abcdef"); + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static + private static ReadOnlySpan Hex => new byte[16] { (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f' }; public static int BeginChunkBytes(int dataCount, Span span) { @@ -28,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http count = (total >> 2) + 3; var offset = 0; - ref var startHex = ref _hex[0]; + ref var startHex = ref MemoryMarshal.GetReference(Hex); for (shift = total; shift >= 0; shift -= 4) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/DateHeaderValueManager.cs b/src/Servers/Kestrel/Core/src/Internal/Http/DateHeaderValueManager.cs index 371033b330..85b9ad8b61 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/DateHeaderValueManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/DateHeaderValueManager.cs @@ -14,7 +14,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http /// internal class DateHeaderValueManager : IHeartbeatHandler { - private static readonly byte[] _datePreambleBytes = Encoding.ASCII.GetBytes("\r\nDate: "); + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static + private static ReadOnlySpan DatePreambleBytes => new byte[8] { (byte)'\r', (byte)'\n', (byte)'D', (byte)'a', (byte)'t', (byte)'e', (byte)':', (byte)' ' }; private DateHeaderValues _dateValues; @@ -38,9 +39,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private void SetDateValues(DateTimeOffset value) { var dateValue = HeaderUtilities.FormatDate(value); - var dateBytes = new byte[_datePreambleBytes.Length + dateValue.Length]; - Buffer.BlockCopy(_datePreambleBytes, 0, dateBytes, 0, _datePreambleBytes.Length); - Encoding.ASCII.GetBytes(dateValue, 0, dateValue.Length, dateBytes, _datePreambleBytes.Length); + var dateBytes = new byte[DatePreambleBytes.Length + dateValue.Length]; + DatePreambleBytes.CopyTo(dateBytes); + Encoding.ASCII.GetBytes(dateValue, dateBytes.AsSpan(DatePreambleBytes.Length)); var dateValues = new DateHeaderValues { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs index 0af675600f..a87a22f56a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs @@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal sealed partial class HttpResponseHeaders : HttpHeaders { + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static private static ReadOnlySpan CrLf => new[] { (byte)'\r', (byte)'\n' }; private static ReadOnlySpan ColonSpace => new[] { (byte)':', (byte)' ' }; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.Generated.cs new file mode 100644 index 0000000000..290fea38f5 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.Generated.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 +{ + internal partial class Http2Connection + { + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static + + private static ReadOnlySpan ClientPrefaceBytes => new byte[24] { (byte)'P', (byte)'R', (byte)'I', (byte)' ', (byte)'*', (byte)' ', (byte)'H', (byte)'T', (byte)'T', (byte)'P', (byte)'/', (byte)'2', (byte)'.', (byte)'0', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n', (byte)'S', (byte)'M', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' }; + private static ReadOnlySpan AuthorityBytes => new byte[10] { (byte)':', (byte)'a', (byte)'u', (byte)'t', (byte)'h', (byte)'o', (byte)'r', (byte)'i', (byte)'t', (byte)'y' }; + private static ReadOnlySpan MethodBytes => new byte[7] { (byte)':', (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d' }; + private static ReadOnlySpan PathBytes => new byte[5] { (byte)':', (byte)'p', (byte)'a', (byte)'t', (byte)'h' }; + private static ReadOnlySpan SchemeBytes => new byte[7] { (byte)':', (byte)'s', (byte)'c', (byte)'h', (byte)'e', (byte)'m', (byte)'e' }; + private static ReadOnlySpan StatusBytes => new byte[7] { (byte)':', (byte)'s', (byte)'t', (byte)'a', (byte)'t', (byte)'u', (byte)'s' }; + private static ReadOnlySpan ConnectionBytes => new byte[10] { (byte)'c', (byte)'o', (byte)'n', (byte)'n', (byte)'e', (byte)'c', (byte)'t', (byte)'i', (byte)'o', (byte)'n' }; + private static ReadOnlySpan TeBytes => new byte[2] { (byte)'t', (byte)'e' }; + private static ReadOnlySpan TrailersBytes => new byte[8] { (byte)'t', (byte)'r', (byte)'a', (byte)'i', (byte)'l', (byte)'e', (byte)'r', (byte)'s' }; + private static ReadOnlySpan ConnectBytes => new byte[7] { (byte)'C', (byte)'O', (byte)'N', (byte)'N', (byte)'E', (byte)'C', (byte)'T' }; + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 52349b04ca..565ac8632d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -27,23 +27,13 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { - internal class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeadersHandler, IRequestProcessor + internal partial class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeadersHandler, IRequestProcessor { - public static byte[] ClientPreface { get; } = Encoding.ASCII.GetBytes("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"); + public static ReadOnlySpan ClientPreface => ClientPrefaceBytes; private static readonly PseudoHeaderFields _mandatoryRequestPseudoHeaderFields = PseudoHeaderFields.Method | PseudoHeaderFields.Path | PseudoHeaderFields.Scheme; - private static readonly byte[] _authorityBytes = Encoding.ASCII.GetBytes(HeaderNames.Authority); - private static readonly byte[] _methodBytes = Encoding.ASCII.GetBytes(HeaderNames.Method); - private static readonly byte[] _pathBytes = Encoding.ASCII.GetBytes(HeaderNames.Path); - private static readonly byte[] _schemeBytes = Encoding.ASCII.GetBytes(HeaderNames.Scheme); - private static readonly byte[] _statusBytes = Encoding.ASCII.GetBytes(HeaderNames.Status); - private static readonly byte[] _connectionBytes = Encoding.ASCII.GetBytes("connection"); - private static readonly byte[] _teBytes = Encoding.ASCII.GetBytes("te"); - private static readonly byte[] _trailersBytes = Encoding.ASCII.GetBytes("trailers"); - private static readonly byte[] _connectBytes = Encoding.ASCII.GetBytes("CONNECT"); - private readonly HttpConnectionContext _context; private readonly Http2FrameWriter _frameWriter; private readonly Pipe _input; @@ -1175,7 +1165,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 if (headerField == PseudoHeaderFields.Method) { - _isMethodConnect = value.SequenceEqual(_connectBytes); + _isMethodConnect = value.SequenceEqual(ConnectBytes); } _parsedPseudoHeaderFields |= headerField; @@ -1217,23 +1207,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return false; } - if (name.SequenceEqual(_pathBytes)) + if (name.SequenceEqual(PathBytes)) { headerField = PseudoHeaderFields.Path; } - else if (name.SequenceEqual(_methodBytes)) + else if (name.SequenceEqual(MethodBytes)) { headerField = PseudoHeaderFields.Method; } - else if (name.SequenceEqual(_schemeBytes)) + else if (name.SequenceEqual(SchemeBytes)) { headerField = PseudoHeaderFields.Scheme; } - else if (name.SequenceEqual(_statusBytes)) + else if (name.SequenceEqual(StatusBytes)) { headerField = PseudoHeaderFields.Status; } - else if (name.SequenceEqual(_authorityBytes)) + else if (name.SequenceEqual(AuthorityBytes)) { headerField = PseudoHeaderFields.Authority; } @@ -1247,7 +1237,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private static bool IsConnectionSpecificHeaderField(ReadOnlySpan name, ReadOnlySpan value) { - return name.SequenceEqual(_connectionBytes) || (name.SequenceEqual(_teBytes) && !value.SequenceEqual(_trailersBytes)); + return name.SequenceEqual(ConnectionBytes) || (name.SequenceEqual(TeBytes) && !value.SequenceEqual(TrailersBytes)); } private bool TryClose() diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index 244bc2663c..415dbf3ba2 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 internal class Http2FrameWriter { // Literal Header Field without Indexing - Indexed Name (Index 8 - :status) + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static private static ReadOnlySpan ContinueBytes => new byte[] { 0x08, 0x03, (byte)'1', (byte)'0', (byte)'0' }; private readonly object _writeLock = new object(); diff --git a/src/Servers/Kestrel/Kestrel/test/GeneratedCodeTests.cs b/src/Servers/Kestrel/Kestrel/test/GeneratedCodeTests.cs index 67969b30f5..0b1a191665 100644 --- a/src/Servers/Kestrel/Kestrel/test/GeneratedCodeTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/GeneratedCodeTests.cs @@ -16,14 +16,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Flaky("https://github.com/aspnet/AspNetCore-Internal/issues/2223", FlakyOn.Helix.All)] public void GeneratedCodeIsUpToDate() { - var httpHeadersGeneratedPath = Path.Combine(AppContext.BaseDirectory,"shared", "GeneratedContent", "HttpHeaders.Generated.cs"); - var httpProtocolGeneratedPath = Path.Combine(AppContext.BaseDirectory,"shared", "GeneratedContent", "HttpProtocol.Generated.cs"); - var httpUtilitiesGeneratedPath = Path.Combine(AppContext.BaseDirectory,"shared", "GeneratedContent", "HttpUtilities.Generated.cs"); - var transportConnectionGeneratedPath = Path.Combine(AppContext.BaseDirectory,"shared", "GeneratedContent", "TransportConnection.Generated.cs"); + var httpHeadersGeneratedPath = Path.Combine(AppContext.BaseDirectory, "shared", "GeneratedContent", "HttpHeaders.Generated.cs"); + var httpProtocolGeneratedPath = Path.Combine(AppContext.BaseDirectory, "shared", "GeneratedContent", "HttpProtocol.Generated.cs"); + var httpUtilitiesGeneratedPath = Path.Combine(AppContext.BaseDirectory, "shared", "GeneratedContent", "HttpUtilities.Generated.cs"); + var http2ConnectionGeneratedPath = Path.Combine(AppContext.BaseDirectory, "shared", "GeneratedContent", "Http2Connection.Generated.cs"); + var transportConnectionGeneratedPath = Path.Combine(AppContext.BaseDirectory, "shared", "GeneratedContent", "TransportConnection.Generated.cs"); var testHttpHeadersGeneratedPath = Path.GetTempFileName(); var testHttpProtocolGeneratedPath = Path.GetTempFileName(); var testHttpUtilitiesGeneratedPath = Path.GetTempFileName(); + var testHttp2ConnectionGeneratedPath = Path.GetTempFileName(); var testTransportConnectionGeneratedPath = Path.GetTempFileName(); try @@ -31,26 +33,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var currentHttpHeadersGenerated = File.ReadAllText(httpHeadersGeneratedPath); var currentHttpProtocolGenerated = File.ReadAllText(httpProtocolGeneratedPath); var currentHttpUtilitiesGenerated = File.ReadAllText(httpUtilitiesGeneratedPath); + var currentHttp2ConnectionGenerated = File.ReadAllText(http2ConnectionGeneratedPath); var currentTransportConnectionGenerated = File.ReadAllText(transportConnectionGeneratedPath); - CodeGenerator.Program.Run(testHttpHeadersGeneratedPath, testHttpProtocolGeneratedPath, testHttpUtilitiesGeneratedPath, testTransportConnectionGeneratedPath); + CodeGenerator.Program.Run(testHttpHeadersGeneratedPath, testHttpProtocolGeneratedPath, testHttpUtilitiesGeneratedPath, testTransportConnectionGeneratedPath, testHttp2ConnectionGeneratedPath); var testHttpHeadersGenerated = File.ReadAllText(testHttpHeadersGeneratedPath); var testHttpProtocolGenerated = File.ReadAllText(testHttpProtocolGeneratedPath); var testHttpUtilitiesGenerated = File.ReadAllText(testHttpUtilitiesGeneratedPath); + var testHttp2ConnectionGenerated = File.ReadAllText(testHttp2ConnectionGeneratedPath); var testTransportConnectionGenerated = File.ReadAllText(testTransportConnectionGeneratedPath); Assert.Equal(currentHttpHeadersGenerated, testHttpHeadersGenerated, ignoreLineEndingDifferences: true); Assert.Equal(currentHttpProtocolGenerated, testHttpProtocolGenerated, ignoreLineEndingDifferences: true); Assert.Equal(currentHttpUtilitiesGenerated, testHttpUtilitiesGenerated, ignoreLineEndingDifferences: true); + Assert.Equal(currentHttp2ConnectionGenerated, testHttp2ConnectionGenerated, ignoreLineEndingDifferences: true); Assert.Equal(currentTransportConnectionGenerated, testTransportConnectionGenerated, ignoreLineEndingDifferences: true); - } finally { File.Delete(testHttpHeadersGeneratedPath); File.Delete(testHttpProtocolGeneratedPath); File.Delete(testHttpUtilitiesGeneratedPath); + File.Delete(testHttp2ConnectionGeneratedPath); File.Delete(testTransportConnectionGeneratedPath); } } diff --git a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj index cade793bc2..33eda7eb1b 100644 --- a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj +++ b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs b/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs index b51e3a5b04..5129df13de 100644 --- a/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs +++ b/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs @@ -340,7 +340,7 @@ namespace http2cat await writableBuffer.FlushAsync().AsTask().DefaultTimeout(); } - public Task SendPreambleAsync() => SendAsync(new ArraySegment(Http2Connection.ClientPreface)); + public Task SendPreambleAsync() => SendAsync(Http2Connection.ClientPreface); public async Task SendSettingsAsync() { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index c6af966746..556e62a07f 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -688,7 +688,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await writableBuffer.FlushAsync().AsTask().DefaultTimeout(); } - protected Task SendPreambleAsync() => SendAsync(new ArraySegment(Http2Connection.ClientPreface)); + protected Task SendPreambleAsync() => SendAsync(Http2Connection.ClientPreface); protected async Task SendSettingsAsync() { diff --git a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj index d0431dbb29..7337ccfc0c 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj +++ b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -18,7 +18,7 @@ $(MSBuildThisFileDirectory)..\..\ - Core/src/Internal/Http/HttpHeaders.Generated.cs Core/src/Internal/Http/HttpProtocol.Generated.cs Core/src/Internal/Infrastructure/HttpUtilities.Generated.cs shared/TransportConnection.Generated.cs + Core/src/Internal/Http/HttpHeaders.Generated.cs Core/src/Internal/Http/HttpProtocol.Generated.cs Core/src/Internal/Infrastructure/HttpUtilities.Generated.cs shared/TransportConnection.Generated.cs Core/src/Internal/Http2/Http2Connection.Generated.cs diff --git a/src/Servers/Kestrel/tools/CodeGenerator/Http2Connection.cs b/src/Servers/Kestrel/tools/CodeGenerator/Http2Connection.cs new file mode 100644 index 0000000000..6cfabb9962 --- /dev/null +++ b/src/Servers/Kestrel/tools/CodeGenerator/Http2Connection.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Net.Http.Headers; + +namespace CodeGenerator +{ + public static class Http2Connection + { + public static string GenerateFile() + { + return ReadOnlySpanStaticDataGenerator.GenerateFile( + namespaceName: "Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2", + className: "Http2Connection", + allProperties: GetStrings()); + } + + private static IEnumerable<(string Name, string Value)> GetStrings() + { + yield return ("ClientPreface", "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"); + yield return ("Authority", HeaderNames.Authority); + yield return ("Method", HeaderNames.Method); + yield return ("Path", HeaderNames.Path); + yield return ("Scheme", HeaderNames.Scheme); + yield return ("Status", HeaderNames.Status); + yield return ("Connection", "connection"); + yield return ("Te", "te"); + yield return ("Trailers", "trailers"); + yield return ("Connect", "CONNECT"); + } + } +} diff --git a/src/Servers/Kestrel/tools/CodeGenerator/Program.cs b/src/Servers/Kestrel/tools/CodeGenerator/Program.cs index eb687fccac..c319d9bfb3 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/Program.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/Program.cs @@ -30,8 +30,13 @@ namespace CodeGenerator Console.Error.WriteLine("Missing path to TransportConnection.Generated.cs"); return 1; } + else if (args.Length < 5) + { + Console.Error.WriteLine("Missing path to Http2Connection.Generated.cs"); + return 1; + } - Run(args[0], args[1], args[2], args[3]); + Run(args[0], args[1], args[2], args[3], args[4]); return 0; } @@ -40,12 +45,14 @@ namespace CodeGenerator string knownHeadersPath, string httpProtocolFeatureCollectionPath, string httpUtilitiesPath, - string transportConnectionFeatureCollectionPath) + string transportConnectionFeatureCollectionPath, + string http2ConnectionPath) { var knownHeadersContent = KnownHeaders.GeneratedFile(); var httpProtocolFeatureCollectionContent = HttpProtocolFeatureCollection.GenerateFile(); var httpUtilitiesContent = HttpUtilities.HttpUtilities.GeneratedFile(); var transportConnectionFeatureCollectionContent = TransportConnectionFeatureCollection.GenerateFile(); + var http2ConnectionContent = Http2Connection.GenerateFile(); var existingKnownHeaders = File.Exists(knownHeadersPath) ? File.ReadAllText(knownHeadersPath) : ""; if (!string.Equals(knownHeadersContent, existingKnownHeaders)) @@ -70,6 +77,12 @@ namespace CodeGenerator { File.WriteAllText(transportConnectionFeatureCollectionPath, transportConnectionFeatureCollectionContent); } + + var existingHttp2Connection = File.Exists(http2ConnectionPath) ? File.ReadAllText(http2ConnectionPath) : ""; + if (!string.Equals(http2ConnectionContent, existingHttp2Connection)) + { + File.WriteAllText(http2ConnectionPath, http2ConnectionContent); + } } } } diff --git a/src/Servers/Kestrel/tools/CodeGenerator/ReadOnlySpanStaticDataGenerator.cs b/src/Servers/Kestrel/tools/CodeGenerator/ReadOnlySpanStaticDataGenerator.cs new file mode 100644 index 0000000000..1689292eb5 --- /dev/null +++ b/src/Servers/Kestrel/tools/CodeGenerator/ReadOnlySpanStaticDataGenerator.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CodeGenerator +{ + public static class ReadOnlySpanStaticDataGenerator + { + public static string GenerateFile(string namespaceName, string className, IEnumerable<(string Name, string Value)> allProperties) + { + var properties = allProperties.Select((p, index) => new Property + { + Data = p, + Index = index + }); + + return $@"// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace {namespaceName} +{{ + internal partial class {className} + {{ + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static + {Each(properties, p => $@" + private static ReadOnlySpan {p.Data.Name}Bytes => new byte[{p.Data.Value.Length}] {{ {GetDataAsBytes(p.Data.Value)} }};")} + }} +}} +"; + } + + private static string Each(IEnumerable values, Func formatter) + { + return values.Any() ? values.Select(formatter).Aggregate((a, b) => a + b) : ""; + } + + private static string GetDataAsBytes(string value) + { + var stringBuilder = new StringBuilder(); + + for (var i = 0; i < value.Length; ++i) + { + var c = value[i]; + if (c == '\n') + { + stringBuilder.Append("(byte)'\\n'"); + } + else if (c == '\r') + { + stringBuilder.Append("(byte)'\\r'"); + } + else + { + stringBuilder.AppendFormat("(byte)'{0}'", c); + } + + if (i < value.Length - 1) + { + stringBuilder.Append(", "); + } + } + + return stringBuilder.ToString(); + } + + private class Property + { + public (string Name, string Value) Data; + public int Index; + } + } +} diff --git a/src/Shared/Http2/Hpack/StatusCodes.cs b/src/Shared/Http2/Hpack/StatusCodes.cs index 602a3c02bc..f302e7339b 100644 --- a/src/Shared/Http2/Hpack/StatusCodes.cs +++ b/src/Shared/Http2/Hpack/StatusCodes.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. // See THIRD-PARTY-NOTICES.TXT in the project root for license information. @@ -9,6 +9,8 @@ namespace System.Net.Http.HPack { internal static class StatusCodes { + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static + private static ReadOnlySpan BytesStatus100 => new byte[] { (byte)'1', (byte)'0', (byte)'0' }; private static ReadOnlySpan BytesStatus101 => new byte[] { (byte)'1', (byte)'0', (byte)'1' }; private static ReadOnlySpan BytesStatus102 => new byte[] { (byte)'1', (byte)'0', (byte)'2' }; diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsMessageParser.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsMessageParser.cs index 6d4b5b8ffb..ed4caa274a 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsMessageParser.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsMessageParser.cs @@ -15,8 +15,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal private const byte ByteLF = (byte)'\n'; private const byte ByteColon = (byte)':'; - private static ReadOnlySpan _dataPrefix => new byte[] { (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)':', (byte)' ' }; - private static ReadOnlySpan _sseLineEnding => new byte[] { (byte)'\r', (byte)'\n' }; + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static + private static ReadOnlySpan DataPrefix => new byte[] { (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)':', (byte)' ' }; + private static ReadOnlySpan SseLineEnding => new byte[] { (byte)'\r', (byte)'\n' }; private static readonly byte[] _newLine = Encoding.UTF8.GetBytes(Environment.NewLine); private InternalParseState _internalParserState = InternalParseState.ReadMessagePayload; @@ -74,7 +75,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal // data: foo\n\bar should be encoded as // data: foo\r\n // data: bar\r\n - else if (line[line.Length - _sseLineEnding.Length] != ByteCR) + else if (line[line.Length - SseLineEnding.Length] != ByteCR) { throw new FormatException("Unexpected '\\n' in message. A '\\n' character can only be used as part of the newline sequence '\\r\\n'"); } @@ -90,8 +91,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal EnsureStartsWithDataPrefix(line); // Slice away the 'data: ' - var payloadLength = line.Length - (_dataPrefix.Length + _sseLineEnding.Length); - var newData = line.Slice(_dataPrefix.Length, payloadLength).ToArray(); + var payloadLength = line.Length - (DataPrefix.Length + SseLineEnding.Length); + var newData = line.Slice(DataPrefix.Length, payloadLength).ToArray(); _data.Add(newData); start = lineEnd; @@ -162,7 +163,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal private void EnsureStartsWithDataPrefix(ReadOnlySpan line) { - if (!line.StartsWith(_dataPrefix)) + if (!line.StartsWith(DataPrefix)) { throw new FormatException("Expected the message prefix 'data: '"); } @@ -170,7 +171,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal private bool IsMessageEnd(ReadOnlySpan line) { - return line.Length == _sseLineEnding.Length && line.SequenceEqual(_sseLineEnding); + return line.Length == SseLineEnding.Length && line.SequenceEqual(SseLineEnding); } public enum ParseResult