Skip to content

Commit

Permalink
[3.1] Add latin1 support to IIS (#22798)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris Ross <Tratcher@Outlook.com>
  • Loading branch information
jkotalik and Tratcher committed Jun 12, 2020
1 parent 0ec79c5 commit dcd1250
Show file tree
Hide file tree
Showing 18 changed files with 220 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<Compile Include="$(SharedSourceRoot)HttpSys\**\*.cs" />
<Compile Include="$(SharedSourceRoot)ServerInfrastructure\*.cs" LinkBase="ServerInfrastructure" />
</ItemGroup>

<ItemGroup>
Expand Down
5 changes: 3 additions & 2 deletions src/Servers/IIS/IIS/src/Core/IISHttpContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ internal unsafe IISHttpContext(
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;
Expand Down
5 changes: 3 additions & 2 deletions src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Buffers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
Expand All @@ -15,8 +16,8 @@ internal class IISHttpContextOfT<TContext> : IISHttpContext
{
private readonly IHttpApplication<TContext> _application;

public IISHttpContextOfT(MemoryPool<byte> memoryPool, IHttpApplication<TContext> application, IntPtr pInProcessHandler, IISServerOptions options, IISHttpServer server, ILogger logger)
: base(memoryPool, pInProcessHandler, options, server, logger)
public IISHttpContextOfT(MemoryPool<byte> memoryPool, IHttpApplication<TContext> application, IntPtr pInProcessHandler, IISServerOptions options, IISHttpServer server, ILogger logger, bool useLatin1)
: base(memoryPool, pInProcessHandler, options, server, logger, useLatin1)
{
_application = application;
}
Expand Down
6 changes: 5 additions & 1 deletion src/Servers/IIS/IIS/src/Core/IISHttpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,14 @@ private static void OnRequestsDrained(IntPtr serverContext)

private class IISContextFactory<T> : IISContextFactory
{
private const string Latin1Suppport = "Microsoft.AspNetCore.Server.IIS.Latin1RequestHeaders";

private readonly IHttpApplication<T> _application;
private readonly MemoryPool<byte> _memoryPool;
private readonly IISServerOptions _options;
private readonly IISHttpServer _server;
private readonly ILogger _logger;
private readonly bool _useLatin1;

public IISContextFactory(MemoryPool<byte> memoryPool, IHttpApplication<T> application, IISServerOptions options, IISHttpServer server, ILogger logger)
{
Expand All @@ -227,11 +230,12 @@ public IISContextFactory(MemoryPool<byte> memoryPool, IHttpApplication<T> applic
_options = options;
_server = server;
_logger = logger;
AppContext.TryGetSwitch(Latin1Suppport, out _useLatin1);
}

public IISHttpContext CreateHttpContext(IntPtr pInProcessHandler)
{
return new IISHttpContextOfT<T>(_memoryPool, _application, pInProcessHandler, _options, _server, _logger);
return new IISHttpContextOfT<T>(_memoryPool, _application, pInProcessHandler, _options, _server, _logger, _useLatin1);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Compile Include="$(SharedSourceRoot)StackTrace\**\*.cs" LinkBase="Shared\" />
<Compile Include="$(SharedSourceRoot)RazorViews\*.cs" LinkBase="Shared\" />
<Compile Include="$(SharedSourceRoot)ErrorPage\*.cs" LinkBase="Shared\" />
<Compile Include="$(SharedSourceRoot)ServerInfrastructure\*.cs" LinkBase="Shared\" />
</ItemGroup>

<Target Name="ValidateNativeComponentsBuilt" AfterTargets="Build" >
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">


<PropertyGroup>
Expand Down Expand Up @@ -27,6 +27,7 @@
<Reference Include="Microsoft.Extensions.Logging.Testing" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="System.Diagnostics.EventLog" />
<Reference Include="System.Net.Http.WinHttpHandler" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
Expand Down Expand Up @@ -34,6 +34,7 @@
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="System.Diagnostics.EventLog" />
<Reference Include="System.Net.WebSockets.WebSocketProtocol" />
<Reference Include="System.Net.Http.WinHttpHandler" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
Expand Down Expand Up @@ -28,6 +28,7 @@
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
<Reference Include="System.Diagnostics.EventLog" />
<Reference Include="System.Net.Http.WinHttpHandler" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="../FunctionalTest.props" />

Expand Down Expand Up @@ -30,6 +30,7 @@
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
<Reference Include="System.Diagnostics.EventLog" />
<Reference Include="System.Net.Http.WinHttpHandler" />
</ItemGroup>

</Project>
23 changes: 20 additions & 3 deletions src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ public static int Main(string[] args)
}

return 0;
#if !FORWARDCOMPAT
case "ThrowInStartupGenericHost":
{
#if !FORWARDCOMPAT
var host = new HostBuilder().ConfigureWebHost((c) =>
{
c.ConfigureLogging((_, factory) =>
Expand All @@ -161,9 +161,26 @@ public static int Main(string[] args)


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

host.Build().Run();
return 0;
}
#endif
default:
return StartServer();

Expand Down
14 changes: 14 additions & 0 deletions src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,5 +1005,19 @@ public async Task HTTPS_PORT(HttpContext context)

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,67 +130,8 @@ public static unsafe string GetAsciiStringNonNullCharacters(this Span<byte> span
return asciiString;
}

private static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this Span<byte> span)
{
if (span.IsEmpty)
{
return string.Empty;
}

var resultString = new string('\0', span.Length);

fixed (char* output = resultString)
fixed (byte* buffer = span)
{
// StringUtilities.TryGetAsciiString returns null if there are any null (0 byte) characters
// in the string
if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length))
{
// null characters are considered invalid
if (span.IndexOf((byte)0) != -1)
{
throw new InvalidOperationException();
}

try
{
resultString = HeaderValueEncoding.GetString(buffer, span.Length);
}
catch (DecoderFallbackException)
{
throw new InvalidOperationException();
}
}
}

return resultString;
}

private static unsafe string GetLatin1StringNonNullCharacters(this Span<byte> 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 Span<byte> span, bool useLatin1) =>
useLatin1 ? GetLatin1StringNonNullCharacters(span) : GetAsciiOrUTF8StringNonNullCharacters(span);
useLatin1 ? span.GetLatin1StringNonNullCharacters() : span.GetAsciiOrUTF8StringNonNullCharacters(HeaderValueEncoding);

public static string GetAsciiStringEscaped(this Span<byte> span, int maxChars)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<Compile Include="$(SharedSourceRoot)CertificateGeneration\**\*.cs" />
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
<Compile Include="$(SharedSourceRoot)UrlDecoder\**\*.cs" />
<Compile Include="$(SharedSourceRoot)ServerInfrastructure\**\*.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
23 changes: 10 additions & 13 deletions src/Shared/HttpSys/RequestProcessing/HeaderEncoding.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
// 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.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)
{
// net451: return new string(pBytes, 0, byteCount, Encoding);

var charCount = Encoding.GetCharCount(pBytes, byteCount);
var chars = new char[charCount];
fixed (char* pChars = chars)
if (useLatin1)
{
return new Span<byte>(pBytes, byteCount).GetLatin1StringNonNullCharacters();
}
else
{
var count = Encoding.GetChars(pBytes, byteCount, pChars, charCount);
System.Diagnostics.Debug.Assert(count == charCount);
return new Span<byte>(pBytes, byteCount).GetAsciiOrUTF8StringNonNullCharacters(Encoding);
}
return new string(chars);
}

internal static byte[] GetBytes(string myString)
Expand Down
Loading

0 comments on commit dcd1250

Please sign in to comment.