diff --git a/IISIntegration.sln b/IISIntegration.sln index 636f15279e..4086f09be5 100644 --- a/IISIntegration.sln +++ b/IISIntegration.sln @@ -72,6 +72,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AspNetCoreModuleTests", "test\AspNetCoreModuleTests\AspNetCoreModuleTests.vcxproj", "{0692D963-DB10-4387-B3EA-460FBB9BD9A3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ANCMStressTestApp", "test\ANCMStressTestApp\ANCMStressTestApp.csproj", "{13FD8F12-FFBE-4D01-B4AC-444F2994B04F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -228,6 +230,18 @@ Global {0692D963-DB10-4387-B3EA-460FBB9BD9A3}.Release|x64.Build.0 = Release|x64 {0692D963-DB10-4387-B3EA-460FBB9BD9A3}.Release|x86.ActiveCfg = Release|Win32 {0692D963-DB10-4387-B3EA-460FBB9BD9A3}.Release|x86.Build.0 = Release|Win32 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x64.ActiveCfg = Debug|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x64.Build.0 = Debug|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x86.ActiveCfg = Debug|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x86.Build.0 = Debug|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|Any CPU.Build.0 = Release|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x64.ActiveCfg = Release|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x64.Build.0 = Release|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x86.ActiveCfg = Release|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -246,6 +260,7 @@ Global {D57EA297-6DC2-4BC0-8C91-334863327863} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} {46A8612B-418B-4D70-B3A7-A21DD0627473} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} {0692D963-DB10-4387-B3EA-460FBB9BD9A3} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F} = {EF30B533-D715-421A-92B7-92FEF460AC9C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DB4F868D-E1AE-4FD7-9333-69FA15B268C5} diff --git a/build/dependencies.props b/build/dependencies.props index 45eb8e351b..07933c585a 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -37,6 +37,7 @@ 4.5.0-preview2-26313-01 6.1.7601.17515 4.5.0-preview2-26313-01 + 4.5.0-preview2-26313-01 4.5.0-preview2-26313-01 4.5.0-preview2-26313-01 4.5.0-preview2-26313-01 diff --git a/test/ANCMStressTestApp/ANCMStressTestApp.csproj b/test/ANCMStressTestApp/ANCMStressTestApp.csproj new file mode 100644 index 0000000000..54755b4824 --- /dev/null +++ b/test/ANCMStressTestApp/ANCMStressTestApp.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp2.1 + win-x86;win-x64 + + + + + + + + + + + + + diff --git a/test/ANCMStressTestApp/Program.cs b/test/ANCMStressTestApp/Program.cs new file mode 100644 index 0000000000..b0edb5b7f2 --- /dev/null +++ b/test/ANCMStressTestApp/Program.cs @@ -0,0 +1,35 @@ +// 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.Linq; +using System.Threading; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.IISIntegration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ANCMStressTestApp +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.AddConsole(); + }) + .UseKestrel() + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/test/ANCMStressTestApp/Properties/launchSettings.json b/test/ANCMStressTestApp/Properties/launchSettings.json new file mode 100644 index 0000000000..8e9b6cfcf7 --- /dev/null +++ b/test/ANCMStressTestApp/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:16606/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ANCMStressTestSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:16607/" + } + } +} \ No newline at end of file diff --git a/test/ANCMStressTestApp/Startup.cs b/test/ANCMStressTestApp/Startup.cs new file mode 100644 index 0000000000..be0969ec77 --- /dev/null +++ b/test/ANCMStressTestApp/Startup.cs @@ -0,0 +1,231 @@ +// 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; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; +using System.Text; +using System.Net.WebSockets; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; + +namespace ANCMStressTestApp +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app) + { + app.Map("/HelloWorld", HelloWorld); + app.Map("/ConnectionClose", ConnectionClose); + app.Map("/EchoPostData", EchoPostData); + app.Map("/LargeResponseBody", LargeResponseBody); + app.Map("/ResponseHeaders", ResponseHeaders); + app.Map("/EnvironmentVariables", EnvironmentVariables); + app.Map("/RequestInformation", RequestInformation); + app.Map("/WebSocket", WebSocket); + + app.Run(async context => + { + await context.Response.WriteAsync("Default Page"); + }); + } + + private void HelloWorld(IApplicationBuilder app) + { + app.Run(async context => + { + await context.Response.WriteAsync("Hello World"); + }); + } + + private void ConnectionClose(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.Headers[HeaderNames.Connection] = "close"; + await context.Response.WriteAsync("Connnection Close"); + await context.Response.Body.FlushAsync(); + }); + } + + private void EchoPostData(IApplicationBuilder app) + { + app.Run(async context => + { + string responseBody = string.Empty; + + if (string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) + { + using (StreamReader reader = new StreamReader(context.Request.Body, Encoding.UTF8)) + { + responseBody = await reader.ReadToEndAsync(); + } + } + else + { + responseBody = "NoAction"; + } + + await context.Response.WriteAsync(responseBody); + }); + } + + private void LargeResponseBody(IApplicationBuilder app) + { + app.Run(async context => + { + if (int.TryParse(context.Request.Query["length"], out var length)) + { + await context.Response.WriteAsync(new string('a', length)); + } + }); + } + + private void ResponseHeaders(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.Headers["UnknownHeader"] = "test123=foo"; + context.Response.ContentType = "text/plain"; + context.Response.Headers["MultiHeader"] = new StringValues(new string[] { "1", "2" }); + await context.Response.WriteAsync("Request Complete"); + }); + } + + private void EnvironmentVariables(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Environment Variables:" + Environment.NewLine); + var vars = Environment.GetEnvironmentVariables(); + foreach (var key in vars.Keys.Cast().OrderBy(key => key, StringComparer.OrdinalIgnoreCase)) + { + var value = vars[key]; + await context.Response.WriteAsync(key + ": " + value + Environment.NewLine); + } + await context.Response.WriteAsync(Environment.NewLine); + }); + } + + private void RequestInformation(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.ContentType = "text/plain"; + + await context.Response.WriteAsync("Address:" + Environment.NewLine); + await context.Response.WriteAsync("Scheme: " + context.Request.Scheme + Environment.NewLine); + await context.Response.WriteAsync("Host: " + context.Request.Headers["Host"] + Environment.NewLine); + await context.Response.WriteAsync("PathBase: " + context.Request.PathBase.Value + Environment.NewLine); + await context.Response.WriteAsync("Path: " + context.Request.Path.Value + Environment.NewLine); + await context.Response.WriteAsync("Query: " + context.Request.QueryString.Value + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Connection:" + Environment.NewLine); + await context.Response.WriteAsync("RemoteIp: " + context.Connection.RemoteIpAddress + Environment.NewLine); + await context.Response.WriteAsync("RemotePort: " + context.Connection.RemotePort + Environment.NewLine); + await context.Response.WriteAsync("LocalIp: " + context.Connection.LocalIpAddress + Environment.NewLine); + await context.Response.WriteAsync("LocalPort: " + context.Connection.LocalPort + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Headers:" + Environment.NewLine); + foreach (var header in context.Request.Headers) + { + await context.Response.WriteAsync(header.Key + ": " + header.Value + Environment.NewLine); + } + await context.Response.WriteAsync(Environment.NewLine); + }); + } + + private void WebSocket(IApplicationBuilder app) + { + app.Run(async context => + { + var upgradeFeature = context.Features.Get(); + + // Generate WebSocket response headers + string key = context.Request.Headers[Constants.Headers.SecWebSocketKey].ToString(); + var responseHeaders = HandshakeHelpers.GenerateResponseHeaders(key); + foreach (var headerPair in responseHeaders) + { + context.Response.Headers[headerPair.Key] = headerPair.Value; + } + + // Upgrade the connection + Stream opaqueTransport = await upgradeFeature.UpgradeAsync(); + + // Get the WebSocket object + var ws = WebSocketProtocol.CreateFromStream(opaqueTransport, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2)); + + var appLifetime = app.ApplicationServices.GetRequiredService(); + + await Echo(ws, appLifetime.ApplicationStopping); + }); + } + + private async Task Echo(WebSocket webSocket, CancellationToken token) + { + try + { + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + bool closeFromServer = false; + string closeFromServerCmd = "CloseFromServer"; + int closeFromServerLength = closeFromServerCmd.Length; + + while (!result.CloseStatus.HasValue && !token.IsCancellationRequested && !closeFromServer) + { + if (result.Count == closeFromServerLength && + Encoding.ASCII.GetString(buffer).Substring(0, result.Count) == closeFromServerCmd) + { + // The client sent "CloseFromServer" text message to request the server to close (a test scenario). + closeFromServer = true; + } + else + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, token); + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + } + } + + if (result.CloseStatus.HasValue) + { + // Client-initiated close handshake + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + else + { + // Server-initiated close handshake due to either of the two conditions: + // (1) The applicaton host is performing a graceful shutdown. + // (2) The client sent "CloseFromServer" text message to request the server to close (a test scenario). + await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeFromServerCmd, CancellationToken.None); + + // The server has sent the Close frame. + // Stop sending but keep receiving until we get the Close frame from the client. + while (!result.CloseStatus.HasValue) + { + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + } + } + catch (Exception e) + { + Console.WriteLine("{0} Exception caught!", e); + } + } + } +} diff --git a/test/ANCMStressTestApp/WebSockets/Constants.cs b/test/ANCMStressTestApp/WebSockets/Constants.cs new file mode 100644 index 0000000000..bcf5462558 --- /dev/null +++ b/test/ANCMStressTestApp/WebSockets/Constants.cs @@ -0,0 +1,17 @@ +// 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. + +namespace ANCMStressTestApp +{ + public static class Constants + { + public static class Headers + { + public const string Upgrade = "Upgrade"; + public const string UpgradeWebSocket = "websocket"; + public const string Connection = "Connection"; + public const string SecWebSocketKey = "Sec-WebSocket-Key"; + public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; + } + } +} diff --git a/test/ANCMStressTestApp/WebSockets/HandshakeHelpers.cs b/test/ANCMStressTestApp/WebSockets/HandshakeHelpers.cs new file mode 100644 index 0000000000..331f415013 --- /dev/null +++ b/test/ANCMStressTestApp/WebSockets/HandshakeHelpers.cs @@ -0,0 +1,42 @@ +// 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.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace ANCMStressTestApp +{ + // Removed all the + internal static class HandshakeHelpers + { + public static IEnumerable> GenerateResponseHeaders(string key) + { + yield return new KeyValuePair(Constants.Headers.Connection, Constants.Headers.Upgrade); + yield return new KeyValuePair(Constants.Headers.Upgrade, Constants.Headers.UpgradeWebSocket); + yield return new KeyValuePair(Constants.Headers.SecWebSocketAccept, CreateResponseKey(key)); + } + + public static string CreateResponseKey(string requestKey) + { + // "The value of this header field is constructed by concatenating /key/, defined above in step 4 + // in Section 4.2.2, with the string "258EAFA5- E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of + // this concatenated value to obtain a 20-byte value and base64-encoding" + // https://tools.ietf.org/html/rfc6455#section-4.2.2 + + if (requestKey == null) + { + throw new ArgumentNullException(nameof(requestKey)); + } + + using (var algorithm = SHA1.Create()) + { + string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + byte[] mergedBytes = Encoding.UTF8.GetBytes(merged); + byte[] hashedBytes = algorithm.ComputeHash(mergedBytes); + return Convert.ToBase64String(hashedBytes); + } + } + } +}