diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameHeaders.Generated.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameHeaders.Generated.cs index c4c43f0eb..a8369ac97 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameHeaders.Generated.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameHeaders.Generated.cs @@ -7772,7 +7772,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 17, 14); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -7798,7 +7798,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 31, 8); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -7819,7 +7819,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 133, 16); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -7845,7 +7845,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 350, 10); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -7877,7 +7877,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 0, 17); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -7898,7 +7898,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 39, 14); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -7919,7 +7919,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 53, 10); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -7940,7 +7940,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 63, 11); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -7966,7 +7966,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 74, 21); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -7987,7 +7987,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 95, 11); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8008,7 +8008,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 106, 7); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8029,7 +8029,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 113, 11); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8050,7 +8050,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 124, 9); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8071,7 +8071,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 149, 20); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8092,7 +8092,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 169, 20); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8113,7 +8113,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 189, 20); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8134,7 +8134,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 209, 15); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8155,7 +8155,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 224, 17); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8176,7 +8176,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 241, 11); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8197,7 +8197,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 252, 17); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8218,7 +8218,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 269, 17); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8239,7 +8239,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 286, 7); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8260,7 +8260,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 293, 8); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8281,7 +8281,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 301, 12); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8302,7 +8302,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 313, 22); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8323,7 +8323,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 335, 15); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8344,7 +8344,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 360, 14); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8365,7 +8365,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 374, 8); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8386,7 +8386,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 382, 20); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8407,7 +8407,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 402, 36); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8428,7 +8428,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 438, 32); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8449,7 +8449,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 470, 32); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8470,7 +8470,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 502, 31); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8491,7 +8491,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 533, 33); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } @@ -8512,7 +8512,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) { output.WriteFast(_headerBytes, 566, 26); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseHeaders.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseHeaders.cs index 4b6d98332..9fc752f6c 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseHeaders.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseHeaders.cs @@ -46,9 +46,9 @@ public void CopyTo(ref WritableBuffer output) if (value != null) { output.WriteFast(_CrLf); - output.WriteAscii(kv.Key); + output.WriteAsciiNoValidation(kv.Key); output.WriteFast(_colonSpace); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/PipelineExtensions.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/PipelineExtensions.cs index 6396b5678..4109119da 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/PipelineExtensions.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/PipelineExtensions.cs @@ -158,7 +158,15 @@ private static unsafe void WriteMultiBuffer(this WritableBuffer buffer, byte[] s } } - public unsafe static void WriteAscii(this WritableBuffer buffer, string data) + /// + /// Write string characters as ASCII without validating that characters fall in the ASCII range + /// + /// + /// ASCII character validation is done by + /// + /// the buffer + /// The string to write + public unsafe static void WriteAsciiNoValidation(this WritableBuffer buffer, string data) { if (string.IsNullOrEmpty(data)) { diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.csproj b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.csproj index 7e3f9f44a..38068b0e2 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.csproj +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.csproj @@ -13,6 +13,7 @@ + diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/xunit.runner.json b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/xunit.runner.json new file mode 100644 index 000000000..c76780941 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json.schemastore.org/xunit.runner.schema", + "methodDisplay": "method", + "longRunningTestSeconds": 60 +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/PipelineExtensionTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/PipelineExtensionTests.cs new file mode 100644 index 000000000..e5227dd63 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/PipelineExtensionTests.cs @@ -0,0 +1,167 @@ +// 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.IO.Pipelines; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Server.KestrelTests +{ + public class PipelineExtensionTests : IDisposable + { + // ulong.MaxValue.ToString().Length + private const int _ulongMaxValueLength = 20; + + private readonly IPipe _pipe; + private readonly PipeFactory _pipeFactory = new PipeFactory(); + + public PipelineExtensionTests() + { + _pipe = _pipeFactory.Create(); + } + + public void Dispose() + { + _pipeFactory.Dispose(); + } + + [Theory] + [InlineData(ulong.MinValue)] + [InlineData(ulong.MaxValue)] + [InlineData(4_8_15_16_23_42)] + public async Task WritesNumericToAscii(ulong number) + { + var writer = _pipe.Writer.Alloc(); + writer.WriteNumeric(number); + await writer.FlushAsync(); + + var reader = await _pipe.Reader.ReadAsync(); + var numAsStr = number.ToString(); + var expected = Encoding.ASCII.GetBytes(numAsStr); + AssertExtensions.Equal(expected, reader.Buffer.Slice(0, numAsStr.Length).ToArray()); + } + + [Theory] + [InlineData(1)] + [InlineData(_ulongMaxValueLength / 2)] + [InlineData(_ulongMaxValueLength - 1)] + public void WritesNumericAcrossSpanBoundaries(int gapSize) + { + var writer = _pipe.Writer.Alloc(100); + // almost fill up the first block + var spacer = new Span(new byte[writer.Buffer.Length - gapSize]); + writer.Write(spacer); + + var bufferLength = writer.Buffer.Length; + writer.WriteNumeric(ulong.MaxValue); + Assert.NotEqual(bufferLength, writer.Buffer.Length); + + writer.FlushAsync().GetAwaiter().GetResult(); + + var reader = _pipe.Reader.ReadAsync().GetAwaiter().GetResult(); + var numAsString = ulong.MaxValue.ToString(); + var written = reader.Buffer.Slice(spacer.Length, numAsString.Length); + Assert.False(written.IsSingleSpan, "The buffer should cross spans"); + AssertExtensions.Equal(Encoding.ASCII.GetBytes(numAsString), written.ToArray()); + } + + [Theory] + [InlineData("\0abcxyz", new byte[] { 0, 97, 98, 99, 120, 121, 122 })] + [InlineData("!#$%i", new byte[] { 33, 35, 36, 37, 105 })] + [InlineData("!#$%", new byte[] { 33, 35, 36, 37 })] + [InlineData("!#$", new byte[] { 33, 35, 36 })] + [InlineData("!#", new byte[] { 33, 35 })] + [InlineData("!", new byte[] { 33 })] + // null or empty + [InlineData("", new byte[0])] + [InlineData(null, new byte[0])] + public async Task EncodesAsAscii(string input, byte[] expected) + { + var writer = _pipe.Writer.Alloc(); + writer.WriteAsciiNoValidation(input); + await writer.FlushAsync(); + var reader = await _pipe.Reader.ReadAsync(); + + if (expected.Length > 0) + { + AssertExtensions.Equal( + expected, + reader.Buffer.ToArray()); + } + else + { + Assert.Equal(0, reader.Buffer.Length); + } + } + + [Theory] + // non-ascii characters stored in 32 bits + [InlineData("𤭢𐐝")] + // non-ascii characters stored in 16 bits + [InlineData("ñ٢⛄⛵")] + public async Task WriteAsciiNoValidationWritesOnlyOneBytePerChar(string input) + { + // WriteAscii doesn't validate if characters are in the ASCII range + // but it shouldn't produce more than one byte per character + var writer = _pipe.Writer.Alloc(); + writer.WriteAsciiNoValidation(input); + await writer.FlushAsync(); + var reader = await _pipe.Reader.ReadAsync(); + + Assert.Equal(input.Length, reader.Buffer.Length); + } + + [Fact] + public async Task WriteAsciiNoValidation() + { + const byte maxAscii = 0x7f; + var writer = _pipe.Writer.Alloc(); + for (var i = 0; i < maxAscii; i++) + { + writer.WriteAsciiNoValidation(new string((char)i, 1)); + } + await writer.FlushAsync(); + + var reader = await _pipe.Reader.ReadAsync(); + var data = reader.Buffer.Slice(0, maxAscii).ToArray(); + for (var i = 0; i < maxAscii; i++) + { + Assert.Equal(i, data[i]); + } + } + + [Theory] + [InlineData(2, 1)] + [InlineData(3, 1)] + [InlineData(4, 2)] + [InlineData(5, 3)] + [InlineData(7, 4)] + [InlineData(8, 3)] + [InlineData(8, 4)] + [InlineData(8, 5)] + [InlineData(100, 48)] + public void WritesAsciiAcrossBlockBoundaries(int stringLength, int gapSize) + { + var testString = new string(' ', stringLength); + var writer = _pipe.Writer.Alloc(100); + // almost fill up the first block + var spacer = new Span(new byte[writer.Buffer.Length - gapSize]); + writer.Write(spacer); + Assert.Equal(gapSize, writer.Buffer.Span.Length); + + var bufferLength = writer.Buffer.Length; + writer.WriteAsciiNoValidation(testString); + Assert.NotEqual(bufferLength, writer.Buffer.Length); + + writer.FlushAsync().GetAwaiter().GetResult(); + + var reader = _pipe.Reader.ReadAsync().GetAwaiter().GetResult(); + var written = reader.Buffer.Slice(spacer.Length, stringLength); + Assert.False(written.IsSingleSpan, "The buffer should cross spans"); + AssertExtensions.Equal(Encoding.ASCII.GetBytes(testString), written.ToArray()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/AssertExtensions.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/AssertExtensions.cs new file mode 100644 index 000000000..cb3fc36a3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/AssertExtensions.cs @@ -0,0 +1,27 @@ +// 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 Xunit.Sdk; + +namespace Xunit +{ + public static class AssertExtensions + { + public static void Equal(byte[] expected, Span actual) + { + if (expected.Length != actual.Length) + { + throw new XunitException($"Expected length to be {expected.Length} but was {actual.Length}"); + } + + for (var i = 0; i < expected.Length; i++) + { + if (expected[i] != actual[i]) + { + throw new XunitException($@"Expected byte at index {i} to be '{expected[i]}' but was '{actual[i]}'"); + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/xunit.runner.json b/test/Microsoft.AspNetCore.Server.KestrelTests/xunit.runner.json index bbd346e81..3a5192e57 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/xunit.runner.json +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/xunit.runner.json @@ -1,4 +1,6 @@ { "$schema": "http://json.schemastore.org/xunit.runner.schema", - "appDomain": "denied" + "appDomain": "denied", + "methodDisplay": "method", + "longRunningTestSeconds": 60 } diff --git a/tools/CodeGenerator/KnownHeaders.cs b/tools/CodeGenerator/KnownHeaders.cs index 5d76a5330..3274b9864 100644 --- a/tools/CodeGenerator/KnownHeaders.cs +++ b/tools/CodeGenerator/KnownHeaders.cs @@ -541,7 +541,7 @@ protected void CopyToFast(ref WritableBuffer output) if (value != null) {{ output.WriteFast(_headerBytes, {header.BytesOffset}, {header.BytesCount}); - output.WriteAscii(value); + output.WriteAsciiNoValidation(value); }} }} }}