Skip to content

Commit

Permalink
Fix IIS outofprocess to remove WebSocket compression handshake (#58846)
Browse files Browse the repository at this point in the history
  • Loading branch information
BrennanConroy authored Nov 13, 2024
1 parent 3277b64 commit f526f0c
Show file tree
Hide file tree
Showing 13 changed files with 226 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,12 @@ public override string ToString()
{
return string.Format(
CultureInfo.InvariantCulture,
"[Variation] :: ServerType={0}, Runtime={1}, Arch={2}, BaseUrlHint={3}, Publish={4}",
"[Variation] :: ServerType={0}, Runtime={1}, Arch={2}, BaseUrlHint={3}, Publish={4}, HostingModel={5}",
ServerType,
RuntimeFlavor,
RuntimeArchitecture,
ApplicationBaseUriHint,
PublishApplicationBeforeDeployment);
PublishApplicationBeforeDeployment,
HostingModel);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ FORWARDING_HANDLER::ExecuteRequestHandler()
if (cchHeader == 9 && _stricmp(pszWebSocketHeader, "websocket") == 0)
{
m_fWebSocketEnabled = TRUE;

// WinHttp does not support any extensions being returned by the server, so we remove the request header to avoid the server
// responding with any accepted extensions.
pRequest->DeleteHeader("Sec-WebSocket-Extensions");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests;
/// This type just maps collection names to available fixtures
/// </summary>
[CollectionDefinition(Name)]
public class IISTestSiteCollection : ICollectionFixture<IISTestSiteFixture>
public class IISTestSiteCollectionInProc : ICollectionFixture<IISTestSiteFixture>
{
public const string Name = nameof(IISTestSiteCollection);
public const string Name = nameof(IISTestSiteCollectionInProc);
}

[CollectionDefinition(Name)]
public class IISTestSiteCollectionOutOfProc : ICollectionFixture<IISTestSiteFixture>
{
public const string Name = nameof(IISTestSiteCollectionOutOfProc);
}

[CollectionDefinition(Name)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ internal IISTestSiteFixture(Action<IISDeploymentParameters> configure)
ApplicationPublisher = new PublishedApplicationPublisher(Helpers.GetInProcessTestSitesName()),
ServerType = DeployerSelector.ServerType
};

// Uncomment to add IIS debug logs to test output.
//DeploymentParameters.EnvironmentVariables.Add("ASPNETCORE_MODULE_DEBUG", "console");

DeploymentParameters.EnableModule("WebSocketModule", "%IIS_BIN%/iiswsock.dll");
}

public HttpClient Client => DeploymentResult.HttpClient;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.IIS.NewShim.FunctionalTests;
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests;
#endif

[Collection(IISTestSiteCollection.Name)]
[Collection(IISTestSiteCollectionInProc.Name)]
[SkipOnHelix("Unsupported queue", Queues = "Windows.Amd64.VS2022.Pre.Open;")]
public class RequestResponseTests
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Server.IIS.FunctionalTests;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.Server.IntegrationTesting;
using Xunit.Abstractions;

#if !IIS_FUNCTIONALS
using Microsoft.AspNetCore.Server.IIS.FunctionalTests;

#if IISEXPRESS_FUNCTIONALS
namespace Microsoft.AspNetCore.Server.IIS.IISExpress.FunctionalTests;
#elif NEWHANDLER_FUNCTIONALS
namespace Microsoft.AspNetCore.Server.IIS.NewHandler.FunctionalTests;
#elif NEWSHIM_FUNCTIONALS
namespace Microsoft.AspNetCore.Server.IIS.NewShim.FunctionalTests;
#endif
#else

namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests;
#endif

[Collection(IISTestSiteCollectionInProc.Name)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win8, SkipReason = "No WebSocket supported on Win7")]
[SkipOnHelix("Unsupported queue", Queues = "Windows.Amd64.VS2022.Pre.Open;")]
public class WebSocketsInProcessTests : WebSocketsTests
{
public WebSocketsInProcessTests(IISTestSiteFixture fixture, ITestOutputHelper testOutput) : base(fixture, testOutput)
{
Fixture.DeploymentParameters.HostingModel = HostingModel.InProcess;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Server.IIS.FunctionalTests;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.Server.IntegrationTesting;
using Xunit.Abstractions;

#if !IIS_FUNCTIONALS
using Microsoft.AspNetCore.Server.IIS.FunctionalTests;

#if IISEXPRESS_FUNCTIONALS
namespace Microsoft.AspNetCore.Server.IIS.IISExpress.FunctionalTests;
#elif NEWHANDLER_FUNCTIONALS
namespace Microsoft.AspNetCore.Server.IIS.NewHandler.FunctionalTests;
#elif NEWSHIM_FUNCTIONALS
namespace Microsoft.AspNetCore.Server.IIS.NewShim.FunctionalTests;
#endif
#else

namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests;
#endif

[Collection(IISTestSiteCollectionOutOfProc.Name)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win8, SkipReason = "No WebSocket supported on Win7")]
[SkipOnHelix("Unsupported queue", Queues = "Windows.Amd64.VS2022.Pre.Open;")]
public class WebSocketsOutOfProcessTests : WebSocketsTests
{
public WebSocketsOutOfProcessTests(IISTestSiteFixture fixture, ITestOutputHelper testOutput) : base(fixture, testOutput)
{
Fixture.DeploymentParameters.HostingModel = HostingModel.OutOfProcess;
}
}
Original file line number Diff line number Diff line change
@@ -1,57 +1,76 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.IIS.FunctionalTests;
using Microsoft.AspNetCore.InternalTesting;
using Xunit;
using Microsoft.AspNetCore.Server.IIS.FunctionalTests;
using Microsoft.AspNetCore.Server.IntegrationTesting;
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
using Xunit.Abstractions;

#if !IIS_FUNCTIONALS
using Microsoft.AspNetCore.Server.IIS.FunctionalTests;

#if IISEXPRESS_FUNCTIONALS
namespace Microsoft.AspNetCore.Server.IIS.IISExpress.FunctionalTests;
#elif NEWHANDLER_FUNCTIONALS
namespace Microsoft.AspNetCore.Server.IIS.NewHandler.FunctionalTests;
#elif NEWSHIM_FUNCTIONALS
namespace Microsoft.AspNetCore.Server.IIS.NewShim.FunctionalTests;
#endif
#else

namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests;
#endif

[Collection(IISTestSiteCollection.Name)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win8, SkipReason = "No WebSocket supported on Win7")]
[SkipOnHelix("Unsupported queue", Queues = "Windows.Amd64.VS2022.Pre.Open;")]
public class WebSocketsTests
public abstract class WebSocketsTests : FunctionalTestsBase
{
private readonly string _requestUri;
private readonly string _webSocketUri;
public IISTestSiteFixture Fixture { get; }

public WebSocketsTests(IISTestSiteFixture fixture)
public WebSocketsTests(IISTestSiteFixture fixture, ITestOutputHelper testOutput) : base(testOutput)
{
_requestUri = fixture.DeploymentResult.ApplicationBaseUri;
_webSocketUri = _requestUri.Replace("http:", "ws:");
Fixture = fixture;
Fixture.DeploymentParameters.EnableLogging("C:/github/aspnetcore/artifacts/log");
}

[ConditionalFact]
public async Task RequestWithBody_NotUpgradable()
{
using var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(200) };
using var response = await client.PostAsync(_requestUri + "WebSocketNotUpgradable", new StringContent("Hello World"));
using var response = await client.PostAsync(Fixture.DeploymentResult.ApplicationBaseUri + "WebSocketNotUpgradable", new StringContent("Hello World"));
response.EnsureSuccessStatusCode();
}

[ConditionalFact]
public async Task RequestWithoutBody_Upgradable()
{
if (Fixture.DeploymentParameters.HostingModel == HostingModel.OutOfProcess)
{
// OutOfProcess doesn't support upgrade requests without the "Upgrade": "websocket" header.
return;
}

using var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(200) };
// POST with Content-Length: 0 counts as not having a body.
using var response = await client.PostAsync(_requestUri + "WebSocketUpgradable", new StringContent(""));
using var response = await client.PostAsync(Fixture.DeploymentResult.ApplicationBaseUri + "WebSocketUpgradable", new StringContent(""));
response.EnsureSuccessStatusCode();
}

[ConditionalFact]
public async Task OnStartedCalledForWebSocket()
{
var cws = new ClientWebSocket();
await cws.ConnectAsync(new Uri(_webSocketUri + "WebSocketLifetimeEvents"), default);
var webSocketUri = Fixture.DeploymentResult.ApplicationBaseUri;
webSocketUri = webSocketUri.Replace("http:", "ws:");

using var cws = new ClientWebSocket();
await cws.ConnectAsync(new Uri(webSocketUri + "WebSocketLifetimeEvents"), default);

await ReceiveMessage(cws, "OnStarting");
await ReceiveMessage(cws, "Upgraded");
Expand All @@ -60,17 +79,23 @@ public async Task OnStartedCalledForWebSocket()
[ConditionalFact]
public async Task WebReadBeforeUpgrade()
{
var cws = new ClientWebSocket();
await cws.ConnectAsync(new Uri(_webSocketUri + "WebSocketReadBeforeUpgrade"), default);
var webSocketUri = Fixture.DeploymentResult.ApplicationBaseUri;
webSocketUri = webSocketUri.Replace("http:", "ws:");

using var cws = new ClientWebSocket();
await cws.ConnectAsync(new Uri(webSocketUri + "WebSocketReadBeforeUpgrade"), default);

await ReceiveMessage(cws, "Yay");
}

[ConditionalFact]
public async Task CanSendAndReceieveData()
{
var cws = new ClientWebSocket();
await cws.ConnectAsync(new Uri(_webSocketUri + "WebSocketEcho"), default);
var webSocketUri = Fixture.DeploymentResult.ApplicationBaseUri;
webSocketUri = webSocketUri.Replace("http:", "ws:");

using var cws = new ClientWebSocket();
await cws.ConnectAsync(new Uri(webSocketUri + "WebSocketEcho"), default);

for (int i = 0; i < 1000; i++)
{
Expand All @@ -80,10 +105,33 @@ public async Task CanSendAndReceieveData()
}
}

[ConditionalFact]
public async Task AttemptCompressionWorks()
{
var webSocketUri = Fixture.DeploymentResult.ApplicationBaseUri;
webSocketUri = webSocketUri.Replace("http:", "ws:");

using var cws = new ClientWebSocket();
cws.Options.DangerousDeflateOptions = new WebSocketDeflateOptions();
await cws.ConnectAsync(new Uri(webSocketUri + "WebSocketAllowCompression"), default);

// Compression doesn't work with OutOfProcess, let's make sure the websocket extensions aren't forwarded and the connection still works
var expected = Fixture.DeploymentParameters.HostingModel == HostingModel.InProcess
? "permessage-deflate; client_max_window_bits=15" : "None";
await ReceiveMessage(cws, expected);

for (int i = 0; i < 1000; i++)
{
var message = i.ToString(CultureInfo.InvariantCulture);
await SendMessage(cws, message);
await ReceiveMessage(cws, message);
}
}

[ConditionalFact]
public async Task Http1_0_Request_NotUpgradable()
{
Uri uri = new Uri(_requestUri + "WebSocketNotUpgradable");
Uri uri = new Uri(Fixture.DeploymentResult.ApplicationBaseUri + "WebSocketNotUpgradable");
using TcpClient client = new TcpClient();

await client.ConnectAsync(uri.Host, uri.Port);
Expand All @@ -103,7 +151,7 @@ public async Task Http1_0_Request_NotUpgradable()
[ConditionalFact]
public async Task Http1_0_Request_UpgradeErrors()
{
Uri uri = new Uri(_requestUri + "WebSocketUpgradeFails");
Uri uri = new Uri(Fixture.DeploymentResult.ApplicationBaseUri + "WebSocketUpgradeFails");
using TcpClient client = new TcpClient();

await client.ConnectAsync(uri.Host, uri.Port);
Expand Down Expand Up @@ -148,6 +196,7 @@ private async Task SendMessage(ClientWebSocket webSocket, string message)

private async Task ReceiveMessage(ClientWebSocket webSocket, string expectedMessage)
{
Debug.Assert(expectedMessage.Length > 0);
var received = new byte[expectedMessage.Length];

var offset = 0;
Expand All @@ -156,7 +205,7 @@ private async Task ReceiveMessage(ClientWebSocket webSocket, string expectedMess
{
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(received, offset, received.Length - offset), default);
offset += result.Count;
} while (!result.EndOfMessage);
} while (!result.EndOfMessage && result.CloseStatus is null && received.Length - offset > 0);

Assert.Equal(expectedMessage, Encoding.ASCII.GetString(received));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>


<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Server.IIS" />
<ProjectReference Include="..\testassets\IIS.Common.TestLib\IIS.Common.TestLib.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
<Reference Include="Microsoft.AspNetCore.HttpsPolicy" />
<Reference Include="Microsoft.AspNetCore.WebSockets" />
<Reference Include="Microsoft.AspNetCore.WebUtilities" />
<Reference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<Reference Include="Microsoft.Extensions.Configuration.Json" />
Expand Down
Loading

0 comments on commit f526f0c

Please sign in to comment.