From 0f622ec53d3c8b8236fc3a6717b386aebf15eed9 Mon Sep 17 00:00:00 2001 From: Yanbing Shi Date: Thu, 22 Mar 2018 11:48:43 -0700 Subject: [PATCH] ANCM stress app --- .../ANCMStressTestSample.csproj | 19 ++ samples/ANCMStressTestSample/Program.cs | 43 ++++ .../Properties/launchSettings.json | 27 +++ samples/ANCMStressTestSample/Startup.cs | 213 ++++++++++++++++++ .../WebSockets/Constants.cs | 21 ++ .../WebSockets/HandshakeHelpers.cs | 42 ++++ 6 files changed, 365 insertions(+) create mode 100644 samples/ANCMStressTestSample/ANCMStressTestSample.csproj create mode 100644 samples/ANCMStressTestSample/Program.cs create mode 100644 samples/ANCMStressTestSample/Properties/launchSettings.json create mode 100644 samples/ANCMStressTestSample/Startup.cs create mode 100644 samples/ANCMStressTestSample/WebSockets/Constants.cs create mode 100644 samples/ANCMStressTestSample/WebSockets/HandshakeHelpers.cs 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); + } + } + } +}