diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs index 6db7988271..4bdfcb78b8 100644 --- a/src/Components/test/E2ETest/Tests/FormsTest.cs +++ b/src/Components/test/E2ETest/Tests/FormsTest.cs @@ -10,6 +10,7 @@ using BasicTestApp.FormsTest; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; +using Microsoft.AspNetCore.Testing; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using Xunit; @@ -190,6 +191,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests } [Fact] + [Flaky("https://github.com/dotnet/aspnetcore-internal/issues/3615", FlakyOn.Helix.All)] public void InputDateInteractsWithEditContext_NonNullableDateTime() { var appElement = MountTypicalValidationComponent(); diff --git a/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/Microsoft.AspNetCore.Components.WebAssembly.Templates.csproj b/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/Microsoft.AspNetCore.Components.WebAssembly.Templates.csproj new file mode 100644 index 0000000000..efde20de6d --- /dev/null +++ b/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/Microsoft.AspNetCore.Components.WebAssembly.Templates.csproj @@ -0,0 +1,49 @@ + + + $(RepoRoot)src\Components\WebAssembly\ + + + + $(DefaultNetCoreTargetFramework) + true + Templates for ASP.NET Core Blazor WebAssembly projects. + $(PackageTags);blazor;spa + $(ComponentsWebAssemblyVersionPrefix) + + + + + + DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework); + MicrosoftEntityFrameworkCoreSqlServerPackageVersion=$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion); + MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion); + MicrosoftEntityFrameworkCoreToolsPackageVersion=$(MicrosoftEntityFrameworkCoreToolsPackageVersion); + MicrosoftExtensionsHttpPackageVersion=$(MicrosoftExtensionsHttpPackageVersion); + SystemNetHttpJsonPackageVersion=$(SystemNetHttpJsonPackageVersion) + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj b/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj index 9877d26985..501aa01125 100644 --- a/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj +++ b/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj @@ -14,9 +14,7 @@ - - - + diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index 96c283c551..ed798ba12e 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -76,8 +76,9 @@ namespace Microsoft.AspNetCore.Server.IIS.Core IntPtr pInProcessHandler, IISServerOptions options, IISHttpServer server, - ILogger logger) - : base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler)) + ILogger logger, + bool useLatin1) + : base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler), useLatin1: useLatin1) { _memoryPool = memoryPool; _pInProcessHandler = pInProcessHandler; diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs index ecd3a86e75..070e13c48f 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -17,8 +18,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { private readonly IHttpApplication _application; - public IISHttpContextOfT(MemoryPool memoryPool, IHttpApplication application, IntPtr pInProcessHandler, IISServerOptions options, IISHttpServer server, ILogger logger) - : base(memoryPool, pInProcessHandler, options, server, logger) + public IISHttpContextOfT(MemoryPool memoryPool, IHttpApplication application, IntPtr pInProcessHandler, IISServerOptions options, IISHttpServer server, ILogger logger, bool useLatin1) + : base(memoryPool, pInProcessHandler, options, server, logger, useLatin1) { _application = application; } diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs index ef4c3aabb8..7724f18300 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs @@ -214,11 +214,14 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private class IISContextFactory : IISContextFactory { + private const string Latin1Suppport = "Microsoft.AspNetCore.Server.IIS.Latin1RequestHeaders"; + private readonly IHttpApplication _application; private readonly MemoryPool _memoryPool; private readonly IISServerOptions _options; private readonly IISHttpServer _server; private readonly ILogger _logger; + private readonly bool _useLatin1; public IISContextFactory(MemoryPool memoryPool, IHttpApplication application, IISServerOptions options, IISHttpServer server, ILogger logger) { @@ -227,11 +230,12 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _options = options; _server = server; _logger = logger; + AppContext.TryGetSwitch(Latin1Suppport, out _useLatin1); } public IISHttpContext CreateHttpContext(IntPtr pInProcessHandler) { - return new IISHttpContextOfT(_memoryPool, _application, pInProcessHandler, _options, _server, _logger); + return new IISHttpContextOfT(_memoryPool, _application, pInProcessHandler, _options, _server, _logger, _useLatin1); } } } diff --git a/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj b/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj index 04fbc62328..d262f5cd0b 100644 --- a/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj +++ b/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj @@ -20,7 +20,8 @@ - + + diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/Latin1Tests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/Latin1Tests.cs new file mode 100644 index 0000000000..646b9c9bd9 --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/Latin1Tests.cs @@ -0,0 +1,82 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess +{ + [Collection(PublishedSitesCollection.Name)] + public class Latin1Tests : IISFunctionalTestBase + { + public Latin1Tests(PublishedSitesFixture fixture) : base(fixture) + { + } + + [ConditionalFact] + [RequiresNewHandler] + public async Task Latin1Works() + { + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.TransformArguments((a, _) => $"{a} AddLatin1"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var client = new HttpClient(new LoggingHandler(new WinHttpHandler() { SendTimeout = TimeSpan.FromMinutes(3) }, deploymentResult.Logger)); + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{deploymentResult.ApplicationBaseUri}Latin1"); + requestMessage.Headers.Add("foo", "£"); + + var result = await client.SendAsync(requestMessage); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + } + + [ConditionalFact] + [RequiresNewHandler] + public async Task Latin1ReplacedWithoutAppContextSwitch() + { + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.TransformArguments((a, _) => $"{a}"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var client = new HttpClient(new LoggingHandler(new WinHttpHandler() { SendTimeout = TimeSpan.FromMinutes(3) }, deploymentResult.Logger)); + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{deploymentResult.ApplicationBaseUri}InvalidCharacter"); + requestMessage.Headers.Add("foo", "£"); + + var result = await client.SendAsync(requestMessage); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + } + + [ConditionalFact] + [RequiresNewHandler] + public async Task Latin1InvalidCharacters_HttpSysRejects() + { + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.TransformArguments((a, _) => $"{a} AddLatin1"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + using (var connection = new TestConnection(deploymentResult.HttpClient.BaseAddress.Port)) + { + await connection.Send( + "GET /ReadAndFlushEcho HTTP/1.1", + "Host: localhost", + "Connection: close", + "foo: £\0a", + "", + ""); + + await connection.ReceiveStartsWith("HTTP/1.1 400 Bad Request"); + } + } + } +} diff --git a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj index 5d689a6bee..0999c4903e 100644 --- a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj +++ b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -25,6 +25,7 @@ + diff --git a/src/Servers/IIS/IIS/test/IIS.NewHandler.FunctionalTests/IIS.NewHandler.FunctionalTests.csproj b/src/Servers/IIS/IIS/test/IIS.NewHandler.FunctionalTests/IIS.NewHandler.FunctionalTests.csproj index 530f6f8bbe..4a5234c6bb 100644 --- a/src/Servers/IIS/IIS/test/IIS.NewHandler.FunctionalTests/IIS.NewHandler.FunctionalTests.csproj +++ b/src/Servers/IIS/IIS/test/IIS.NewHandler.FunctionalTests/IIS.NewHandler.FunctionalTests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -33,6 +33,7 @@ + diff --git a/src/Servers/IIS/IIS/test/IIS.NewShim.FunctionalTests/IIS.NewShim.FunctionalTests.csproj b/src/Servers/IIS/IIS/test/IIS.NewShim.FunctionalTests/IIS.NewShim.FunctionalTests.csproj index 07b99fa8d5..6ab82203b9 100644 --- a/src/Servers/IIS/IIS/test/IIS.NewShim.FunctionalTests/IIS.NewShim.FunctionalTests.csproj +++ b/src/Servers/IIS/IIS/test/IIS.NewShim.FunctionalTests/IIS.NewShim.FunctionalTests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -27,6 +27,7 @@ + diff --git a/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj b/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj index d874d0093e..f5a90fc3f3 100644 --- a/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj +++ b/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj @@ -1,4 +1,4 @@ - + @@ -29,6 +29,7 @@ + diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Program.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Program.cs index 4b9889a141..0132efc2e7 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Program.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Program.cs @@ -145,9 +145,9 @@ namespace TestSite } return 0; +#if !FORWARDCOMPAT case "ThrowInStartupGenericHost": { -#if !FORWARDCOMPAT var host = new HostBuilder().ConfigureWebHost((c) => { c.ConfigureLogging((_, factory) => @@ -161,9 +161,26 @@ namespace TestSite host.Build().Run(); -#endif + return 0; } - return 0; + case "AddLatin1": + { + AppContext.SetSwitch("Microsoft.AspNetCore.Server.IIS.Latin1RequestHeaders", isEnabled: true); + var host = new HostBuilder().ConfigureWebHost((c) => + { + c.ConfigureLogging((_, factory) => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseIIS() + .UseStartup(); + }); + + host.Build().Run(); + return 0; + } +#endif default: return StartServer(); diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs index b2ee3c1456..03fc108b92 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs @@ -1016,5 +1016,19 @@ namespace TestSite await context.Response.WriteAsync(httpsPort.HasValue ? httpsPort.Value.ToString() : "NOVALUE"); } + + public Task Latin1(HttpContext context) + { + var value = context.Request.Headers["foo"]; + Assert.Equal("£", value); + return Task.CompletedTask; + } + + public Task InvalidCharacter(HttpContext context) + { + var value = context.Request.Headers["foo"]; + Assert.Equal("�", value); + return Task.CompletedTask; + } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs index 38cd1961ff..39f38b4e21 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs @@ -133,84 +133,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } } - private static string GetAsciiOrUTF8StringNonNullCharacters(this Span span) - => GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan)span); - - public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this ReadOnlySpan span) - { - if (span.IsEmpty) - { - return string.Empty; - } - - fixed (byte* source = &MemoryMarshal.GetReference(span)) - { - var resultString = string.Create(span.Length, new IntPtr(source), s_getAsciiOrUtf8StringNonNullCharacters); - - // If resultString is marked, perform UTF-8 encoding - if (resultString[0] == '\0') - { - // null characters are considered invalid - if (span.IndexOf((byte)0) != -1) - { - throw new InvalidOperationException(); - } - - try - { - resultString = HeaderValueEncoding.GetString(span); - } - catch (DecoderFallbackException) - { - throw new InvalidOperationException(); - } - } - - return resultString; - } - } - - private static readonly SpanAction s_getAsciiOrUtf8StringNonNullCharacters = GetAsciiOrUTF8StringNonNullCharacters; - - private static unsafe void GetAsciiOrUTF8StringNonNullCharacters(Span buffer, IntPtr state) - { - fixed (char* output = &MemoryMarshal.GetReference(buffer)) - { - // This version if AsciiUtilities returns null if there are any null (0 byte) characters - // in the string - if (!StringUtilities.TryGetAsciiString((byte*)state.ToPointer(), output, buffer.Length)) - { - // Mark resultString for UTF-8 encoding - output[0] = '\0'; - } - } - } - - private static unsafe string GetLatin1StringNonNullCharacters(this ReadOnlySpan span) - { - if (span.IsEmpty) - { - return string.Empty; - } - - var resultString = new string('\0', span.Length); - - fixed (char* output = resultString) - fixed (byte* buffer = span) - { - // This returns false if there are any null (0 byte) characters in the string. - if (!StringUtilities.TryGetLatin1String(buffer, output, span.Length)) - { - // null characters are considered invalid - throw new InvalidOperationException(); - } - } - - return resultString; - } - - public static string GetRequestHeaderStringNonNullCharacters(this ReadOnlySpan span, bool useLatin1) => - useLatin1 ? GetLatin1StringNonNullCharacters(span) : GetAsciiOrUTF8StringNonNullCharacters(span); + public static string GetRequestHeaderStringNonNullCharacters(this Span span, bool useLatin1) => + useLatin1 ? span.GetLatin1StringNonNullCharacters() : span.GetAsciiOrUTF8StringNonNullCharacters(HeaderValueEncoding); public static string GetAsciiStringEscaped(this ReadOnlySpan span, int maxChars) { diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index 73f77bb773..59c2a87e41 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Shared/HttpSys/RequestProcessing/HeaderEncoding.cs b/src/Shared/HttpSys/RequestProcessing/HeaderEncoding.cs index a5294c5eb7..14eff1ebce 100644 --- a/src/Shared/HttpSys/RequestProcessing/HeaderEncoding.cs +++ b/src/Shared/HttpSys/RequestProcessing/HeaderEncoding.cs @@ -3,19 +3,24 @@ using System; using System.Text; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.HttpSys.Internal { internal static class HeaderEncoding { - // It should just be ASCII or ANSI, but they break badly with un-expected values. We use UTF-8 because it's the same for - // ASCII, and because some old client would send UTF8 Host headers and expect UTF8 Location responses - // (e.g. IE and HttpWebRequest on intranets). - private static Encoding Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); + private static readonly Encoding Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); - internal static unsafe string GetString(byte* pBytes, int byteCount) + internal static unsafe string GetString(byte* pBytes, int byteCount, bool useLatin1) { - return Encoding.GetString(new ReadOnlySpan(pBytes, byteCount)); + if (useLatin1) + { + return new ReadOnlySpan(pBytes, byteCount).GetLatin1StringNonNullCharacters(); + } + else + { + return new ReadOnlySpan(pBytes, byteCount).GetAsciiOrUTF8StringNonNullCharacters(Encoding); + } } internal static byte[] GetBytes(string myString) diff --git a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs index 88a078fbe2..b9a75b4ba9 100644 --- a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs +++ b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs @@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal private const int AlignmentPadding = 8; private const int DefaultBufferSize = 4096 - AlignmentPadding; private IntPtr _originalBufferAddress; + private bool _useLatin1; private HttpApiTypes.HTTP_REQUEST* _nativeRequest; private IMemoryOwner _backingBuffer; private MemoryHandle _memoryHandle; @@ -61,8 +62,9 @@ namespace Microsoft.AspNetCore.HttpSys.Internal } // To be used by IIS Integration. - internal NativeRequestContext(HttpApiTypes.HTTP_REQUEST* request) + internal NativeRequestContext(HttpApiTypes.HTTP_REQUEST* request, bool useLatin1) { + _useLatin1 = useLatin1; _nativeRequest = request; _bufferAlignment = 0; _permanentlyPinned = true; @@ -155,7 +157,8 @@ namespace Microsoft.AspNetCore.HttpSys.Internal } else if (verb == HttpApiTypes.HTTP_VERB.HttpVerbUnknown && NativeRequest->pUnknownVerb != null) { - return HeaderEncoding.GetString(NativeRequest->pUnknownVerb, NativeRequest->UnknownVerbLength); + // Never use Latin1 for the VERB + return HeaderEncoding.GetString(NativeRequest->pUnknownVerb, NativeRequest->UnknownVerbLength, useLatin1: false); } return null; @@ -321,7 +324,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal // pRawValue will point to empty string ("\0") if (pKnownHeader->RawValueLength > 0) { - value = HeaderEncoding.GetString(pKnownHeader->pRawValue + fixup, pKnownHeader->RawValueLength); + value = HeaderEncoding.GetString(pKnownHeader->pRawValue + fixup, pKnownHeader->RawValueLength, _useLatin1); } return value; @@ -359,11 +362,11 @@ namespace Microsoft.AspNetCore.HttpSys.Internal // pRawValue will be null. if (pUnknownHeader->pName != null && pUnknownHeader->NameLength > 0) { - var headerName = HeaderEncoding.GetString(pUnknownHeader->pName + fixup, pUnknownHeader->NameLength); + var headerName = HeaderEncoding.GetString(pUnknownHeader->pName + fixup, pUnknownHeader->NameLength, _useLatin1); string headerValue; if (pUnknownHeader->pRawValue != null && pUnknownHeader->RawValueLength > 0) { - headerValue = HeaderEncoding.GetString(pUnknownHeader->pRawValue + fixup, pUnknownHeader->RawValueLength); + headerValue = HeaderEncoding.GetString(pUnknownHeader->pRawValue + fixup, pUnknownHeader->RawValueLength, _useLatin1); } else { diff --git a/src/Shared/ServerInfrastructure/StringUtilities.cs b/src/Shared/ServerInfrastructure/StringUtilities.cs index 5468ae3c9a..0d556f9fe4 100644 --- a/src/Shared/ServerInfrastructure/StringUtilities.cs +++ b/src/Shared/ServerInfrastructure/StringUtilities.cs @@ -15,6 +15,85 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure { internal static class StringUtilities { + private static string GetAsciiOrUTF8StringNonNullCharacters(this Span span, Encoding defaultEncoding) + => GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan)span); + + public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this ReadOnlySpan span, Encoding defaultEncoding) + { + if (span.IsEmpty) + { + return string.Empty; + } + + fixed (byte* source = &MemoryMarshal.GetReference(span)) + { + var resultString = string.Create(span.Length, new IntPtr(source), s_getAsciiOrUtf8StringNonNullCharacters); + + // If resultString is marked, perform UTF-8 encoding + if (resultString[0] == '\0') + { + // null characters are considered invalid + if (span.IndexOf((byte)0) != -1) + { + throw new InvalidOperationException(); + } + + try + { + resultString = defaultEncoding.GetString(span); + } + catch (DecoderFallbackException) + { + throw new InvalidOperationException(); + } + } + + return resultString; + } + + return resultString; + } + + + private static readonly SpanAction s_getAsciiOrUtf8StringNonNullCharacters = GetAsciiOrUTF8StringNonNullCharacters; + + private static unsafe void GetAsciiOrUTF8StringNonNullCharacters(Span buffer, IntPtr state) + { + fixed (char* output = &MemoryMarshal.GetReference(buffer)) + { + // This version if AsciiUtilities returns null if there are any null (0 byte) characters + // in the string + if (!StringUtilities.TryGetAsciiString((byte*)state.ToPointer(), output, buffer.Length)) + { + // Mark resultString for UTF-8 encoding + output[0] = '\0'; + } + } + } + + public static unsafe string GetLatin1StringNonNullCharacters(this Span span) + { + if (span.IsEmpty) + { + return string.Empty; + } + + var resultString = new string('\0', span.Length); + + fixed (char* output = resultString) + fixed (byte* buffer = span) + { + // This returns false if there are any null (0 byte) characters in the string. + if (!TryGetLatin1String(buffer, output, span.Length)) + { + // null characters are considered invalid + throw new InvalidOperationException(); + } + } + + return resultString; + } + [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static unsafe bool TryGetAsciiString(byte* input, char* output, int count) { @@ -626,7 +705,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure { // These must be explicity typed as ReadOnlySpan // They then become a non-allocating mappings to the data section of the assembly. - // 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 + // 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 ReadOnlySpan shuffleMaskData = new byte[16] { 0xF, 0xF, 3, 0xF, diff --git a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj index b80599128b..43f275a361 100644 --- a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj @@ -25,6 +25,7 @@ +