diff --git a/samples/ANCMStressTestSample/ANCMStressTestSample.csproj b/samples/ANCMStressTestSample/ANCMStressTestSample.csproj
new file mode 100644
index 0000000000..b1a0c4ec0a
--- /dev/null
+++ b/samples/ANCMStressTestSample/ANCMStressTestSample.csproj
@@ -0,0 +1,19 @@
+
+
+
+ ANCMStressTestApp
+ netcoreapp2.1
+ win-x86;win-x64
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ANCMStressTestSample/Program.cs b/samples/ANCMStressTestSample/Program.cs
new file mode 100644
index 0000000000..1591952577
--- /dev/null
+++ b/samples/ANCMStressTestSample/Program.cs
@@ -0,0 +1,43 @@
+// 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 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 IApplicationLifetime AppLifetime;
+ public static bool AppLifetimeStopping = false;
+
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging((_, factory) =>
+ {
+ factory.AddConsole();
+ })
+ .UseKestrel()
+ .UseStartup()
+ .Build();
+
+ AppLifetime = (IApplicationLifetime)host.Services.GetService(typeof(IApplicationLifetime));
+ AppLifetime.ApplicationStopping.Register(
+ () => {
+ AppLifetimeStopping = true;
+ }
+ );
+
+ host.Run();
+ }
+ }
+}
diff --git a/samples/ANCMStressTestSample/Properties/launchSettings.json b/samples/ANCMStressTestSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..8e9b6cfcf7
--- /dev/null
+++ b/samples/ANCMStressTestSample/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/samples/ANCMStressTestSample/Startup.cs b/samples/ANCMStressTestSample/Startup.cs
new file mode 100644
index 0000000000..cfabe95866
--- /dev/null
+++ b/samples/ANCMStressTestSample/Startup.cs
@@ -0,0 +1,213 @@
+// 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 = string.Join(", ", context.Request.Headers[Constants.Headers.SecWebSocketKey]);
+ 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));
+
+ await Echo(ws);
+ });
+ }
+
+ private async Task Echo(WebSocket webSocket)
+ {
+ var buffer = new byte[1024 * 4];
+ var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
+ bool closeFromServer = false;
+ string closeFromServerCmd = "CloseFromServer";
+ int closeFromServerLength = closeFromServerCmd.Length;
+
+ while (!result.CloseStatus.HasValue)
+ {
+ if ((result.Count == closeFromServerLength && System.Text.Encoding.ASCII.GetString(buffer).Substring(0, result.Count) == closeFromServerCmd)
+ || Program.AppLifetimeStopping == true)
+ {
+ // start closing handshake from backend process when client send "CloseFromServer" text message
+ // or when any message is sent from client during the graceful shutdown.
+ closeFromServer = true;
+ await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeFromServerCmd, CancellationToken.None);
+ }
+ else
+ {
+ await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
+ }
+
+ result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
+ }
+
+ if (!closeFromServer)
+ {
+ await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
+
+ }
+
+ webSocket.Dispose();
+ }
+ }
+}
diff --git a/samples/ANCMStressTestSample/WebSockets/Constants.cs b/samples/ANCMStressTestSample/WebSockets/Constants.cs
new file mode 100644
index 0000000000..43b1822ca9
--- /dev/null
+++ b/samples/ANCMStressTestSample/WebSockets/Constants.cs
@@ -0,0 +1,21 @@
+// 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 ConnectionUpgrade = "Upgrade";
+ public const string SecWebSocketKey = "Sec-WebSocket-Key";
+ public const string SecWebSocketVersion = "Sec-WebSocket-Version";
+ public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol";
+ public const string SecWebSocketAccept = "Sec-WebSocket-Accept";
+ public const string SupportedVersion = "13";
+ }
+ }
+}
diff --git a/samples/ANCMStressTestSample/WebSockets/HandshakeHelpers.cs b/samples/ANCMStressTestSample/WebSockets/HandshakeHelpers.cs
new file mode 100644
index 0000000000..a9d5b61b03
--- /dev/null
+++ b/samples/ANCMStressTestSample/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.ConnectionUpgrade);
+ 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);
+ }
+ }
+ }
+}