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);
}}
}}
}}