Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Httpstress: Add checksum validation & support plaintext http #40360

Merged
Merged
Binary file added src/System.Net.Http/.ionide/symbolCache.db
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Text;
using Crc32 = Force.Crc32.Crc32Algorithm;

namespace HttpStress
{
public static class ChecksumHelpers
{
public static void Append(byte[] bytes, ref uint accumulator)
{
accumulator = Crc32.Append(accumulator, bytes);
}

public static void Append(string text, ref uint accumulator, Encoding encoding = null)
{
if (encoding == null) encoding = Encoding.ASCII;
accumulator = Crc32.Append(accumulator, encoding.GetBytes(text));
}

public static uint Compute(byte[] bytes)
{
return Crc32.Compute(bytes);
}

public static uint ComputeHeaderChecksum<T>(IEnumerable<(string name, T)> headers) where T : IEnumerable<string>
{
uint checksum = 0;

foreach ((string name, IEnumerable<string> values) in headers)
{
Append(name, ref checksum);
foreach (string value in values) Append(value, ref checksum);
}

return checksum;
}
}
}
155 changes: 107 additions & 48 deletions src/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ public class Configuration
public bool ListOperations { get; set; }

public Version HttpVersion { get; set; }
public bool UseWinHttpHandler { get; set; }
public int ConcurrentRequests { get; set; }
public int RandomSeed { get; set; }
public int MaxContentLength { get; set; }
public int MaxRequestUriSize { get; set; }
public int MaxRequestHeaderCount { get; set; }
public int MaxRequestHeaderTotalSize { get; set; }
public int MaxParameters { get; set; }
public int[] OpIndices { get; set; }
public int[] ExcludedOpIndices { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

<ItemGroup>
<PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" />
<PackageReference Include="Crc32.NET" Version="1.2.0" />
<PackageReference Include="System.Net.Http.WinHttpHandler" Version="4.5.4" />
</ItemGroup>

</Project>
12 changes: 9 additions & 3 deletions src/System.Net.Http/tests/StressTests/HttpStress/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ private static bool TryParseCli(string[] args, out Configuration config)
cmd.AddOption(new Option("-serverUri", "Stress suite server uri.") { Argument = new Argument<Uri>("serverUri", new Uri("https://localhost:5001")) });
cmd.AddOption(new Option("-runMode", "Stress suite execution mode. Defaults to Both.") { Argument = new Argument<RunMode>("runMode", RunMode.both) });
cmd.AddOption(new Option("-maxContentLength", "Max content length for request and response bodies.") { Argument = new Argument<int>("numBytes", 1000) });
cmd.AddOption(new Option("-maxRequestUriSize", "Max query string length support by the server.") { Argument = new Argument<int>("numChars", 8000) });
cmd.AddOption(new Option("-maxRequestUriSize", "Max query string length support by the server.") { Argument = new Argument<int>("numChars", 5000) });
cmd.AddOption(new Option("-maxRequestHeaderCount", "Maximum number of headers to place in request") { Argument = new Argument<int>("numHeaders", 90) });
cmd.AddOption(new Option("-maxRequestHeaderTotalSize", "Max request header total size.") { Argument = new Argument<int>("numBytes", 1000) });
cmd.AddOption(new Option("-http", "HTTP version (1.1 or 2.0)") { Argument = new Argument<Version>("version", HttpVersion.Version20) });
cmd.AddOption(new Option("-connectionLifetime", "Max connection lifetime length (milliseconds).") { Argument = new Argument<int?>("connectionLifetime", null) });
cmd.AddOption(new Option("-ops", "Indices of the operations to use") { Argument = new Argument<int[]>("space-delimited indices", null) });
Expand All @@ -47,6 +49,7 @@ private static bool TryParseCli(string[] args, out Configuration config)
cmd.AddOption(new Option("-numParameters", "Max number of query parameters or form fields for a request.") { Argument = new Argument<int>("queryParameters", 1) });
cmd.AddOption(new Option("-cancelRate", "Number between 0 and 1 indicating rate of client-side request cancellation attempts. Defaults to 0.1.") { Argument = new Argument<double>("probability", 0.1) });
cmd.AddOption(new Option("-httpSys", "Use http.sys instead of Kestrel.") { Argument = new Argument<bool>("enable", false) });
cmd.AddOption(new Option("-winHttp", "Use WinHttpHandler for the stress client.") { Argument = new Argument<bool>("enable", false) });
cmd.AddOption(new Option("-displayInterval", "Client stats display interval in seconds. Defaults to 5 seconds.") { Argument = new Argument<int>("seconds", 5) });
cmd.AddOption(new Option("-clientTimeout", "Default HttpClient timeout in seconds. Defaults to 10 seconds.") { Argument = new Argument<int>("seconds", 10) });

Expand All @@ -70,10 +73,13 @@ private static bool TryParseCli(string[] args, out Configuration config)
ListOperations = cmdline.ValueForOption<bool>("-listOps"),

HttpVersion = cmdline.ValueForOption<Version>("-http"),
UseWinHttpHandler = cmdline.ValueForOption<bool>("-winHttp"),
ConcurrentRequests = cmdline.ValueForOption<int>("-n"),
RandomSeed = cmdline.ValueForOption<int?>("-seed") ?? new Random().Next(),
MaxContentLength = cmdline.ValueForOption<int>("-maxContentLength"),
MaxRequestUriSize = cmdline.ValueForOption<int>("-maxRequestUriSize"),
MaxRequestHeaderCount = cmdline.ValueForOption<int>("-maxRequestHeaderCount"),
MaxRequestHeaderTotalSize = cmdline.ValueForOption<int>("-maxRequestHeaderTotalSize"),
OpIndices = cmdline.ValueForOption<int[]>("-ops"),
ExcludedOpIndices = cmdline.ValueForOption<int[]>("-xops"),
MaxParameters = cmdline.ValueForOption<int>("-numParameters"),
Expand Down Expand Up @@ -104,9 +110,9 @@ private static async Task Run(Configuration config)
return;
}

if (config.ServerUri.Scheme != "https")
if (!config.ServerUri.Scheme.StartsWith("http"))
{
Console.Error.WriteLine("Server uri must be https.");
Console.Error.WriteLine("Invalid server uri");
return;
}

Expand Down
69 changes: 47 additions & 22 deletions src/System.Net.Http/tests/StressTests/HttpStress/StressClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
Expand All @@ -19,6 +18,8 @@ namespace HttpStress
{
public class StressClient : IDisposable
{
private const string UNENCRYPTED_HTTP2_ENV_VAR = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2UNENCRYPTEDSUPPORT";

private readonly (string name, Func<RequestContext, Task> operation)[] _clientOperations;
private readonly Configuration _config;
private readonly StressResultAggregator _aggregator;
Expand Down Expand Up @@ -77,16 +78,34 @@ public void PrintFinalReport()

private async Task StartCore()
{
var handler = new SocketsHttpHandler()
if (_config.ServerUri.Scheme == "http")
{
Environment.SetEnvironmentVariable(UNENCRYPTED_HTTP2_ENV_VAR, "1");
}

HttpMessageHandler CreateHttpHandler()
{
PooledConnectionLifetime = _config.ConnectionLifetime.GetValueOrDefault(Timeout.InfiniteTimeSpan),
SslOptions = new SslClientAuthenticationOptions
if (_config.UseWinHttpHandler)
{
RemoteCertificateValidationCallback = delegate { return true; }
return new System.Net.Http.WinHttpHandler()
{
ServerCertificateValidationCallback = delegate { return true; }
};
}
else
{
return new SocketsHttpHandler()
{
PooledConnectionLifetime = _config.ConnectionLifetime.GetValueOrDefault(Timeout.InfiniteTimeSpan),
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = delegate { return true; }
}
};
}
};
}

using var client = new HttpClient(handler) { BaseAddress = _config.ServerUri, Timeout = _config.DefaultTimeout };
using var client = new HttpClient(CreateHttpHandler()) { BaseAddress = _config.ServerUri, Timeout = _config.DefaultTimeout };

async Task RunWorker(int taskNum)
{
Expand Down Expand Up @@ -148,15 +167,16 @@ private sealed class StressFailureType
{
// Representative error text of stress failure
public string ErrorText { get; }
public ImmutableDictionary<int, int> Failures { get; }
// Operation id => failure timestamps
public Dictionary<int, List<DateTime>> Failures { get; }

public StressFailureType(string errorText, ImmutableDictionary<int, int> failures)
public StressFailureType(string errorText)
{
ErrorText = errorText;
Failures = failures;
Failures = new Dictionary<int, List<DateTime>>();
}

public int FailureCount => Failures.Values.Sum();
public int FailureCount => Failures.Values.Select(x => x.Count).Sum();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public int FailureCount => Failures.Values.Select(x => x.Count).Sum();
public int FailureCount => Failures.Values.Sum(x => x.Count);

}

private sealed class StressResultAggregator
Expand Down Expand Up @@ -198,6 +218,8 @@ public void RecordCancellation(int operationIndex, TimeSpan elapsed)

public void RecordFailure(Exception exn, int operationIndex, TimeSpan elapsed, int taskNum, long iteration)
{
DateTime timestamp = DateTime.Now;

Interlocked.Increment(ref _totalRequests);
Interlocked.Increment(ref _failures[operationIndex]);

Expand All @@ -211,17 +233,19 @@ void RecordFailureType()
{
(Type, string, string)[] key = ClassifyFailure(exn);

_failureTypes.AddOrUpdate(key, Add, Update);
StressFailureType failureType = _failureTypes.GetOrAdd(key, _ => new StressFailureType(exn.ToString()));

StressFailureType Add<T>(T key)
lock (failureType)
{
return new StressFailureType(exn.ToString(), ImmutableDictionary<int, int>.Empty.SetItem(operationIndex, 1));
}
List<DateTime> timestamps;

StressFailureType Update<T>(T key, StressFailureType current)
{
current.Failures.TryGetValue(operationIndex, out int failureCount);
return new StressFailureType(current.ErrorText, current.Failures.SetItem(operationIndex, failureCount + 1));
if(!failureType.Failures.TryGetValue(operationIndex, out timestamps))
{
timestamps = new List<DateTime>();
failureType.Failures.Add(operationIndex, timestamps);
}

timestamps.Add(timestamp);
}

(Type exception, string message, string callSite)[] ClassifyFailure(Exception exn)
Expand Down Expand Up @@ -361,15 +385,16 @@ public void PrintFailureTypes()
Console.WriteLine(failure.ErrorText);
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Yellow;
foreach (KeyValuePair<int, int> operation in failure.Failures)
foreach (KeyValuePair<int, List<DateTime>> operation in failure.Failures)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write($"\t{_operationNames[operation.Key].PadRight(30)}");
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.Red;
Console.Write($"Fail: ");
Console.Write("Fail: ");
Console.ResetColor();
Console.WriteLine(operation.Value);
Console.Write(operation.Value.Count);
Console.WriteLine($"\tTimestamps: {string.Join(", ", operation.Value.Select(x => x.ToString("HH:mm:ss")))}");
}

Console.ForegroundColor = ConsoleColor.Cyan;
Expand Down
54 changes: 40 additions & 14 deletions src/System.Net.Http/tests/StressTests/HttpStress/StressServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Server.Kestrel.Core;

namespace HttpStress
{
Expand Down Expand Up @@ -66,24 +67,36 @@ public StressServer(Configuration configuration)
{
// conservative estimation based on https://github.com/aspnet/AspNetCore/blob/caa910ceeba5f2b2c02c47a23ead0ca31caea6f0/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs#L204
ko.Limits.MaxRequestLineSize = Math.Max(ko.Limits.MaxRequestLineSize, configuration.MaxRequestUriSize + 100);
ko.Limits.MaxRequestHeaderCount = Math.Max(ko.Limits.MaxRequestHeaderCount, configuration.MaxRequestHeaderCount);
ko.Limits.MaxRequestHeadersTotalSize = Math.Max(ko.Limits.MaxRequestHeadersTotalSize, configuration.MaxRequestHeaderTotalSize);

IPAddress iPAddress = Dns.GetHostAddresses(configuration.ServerUri.Host).First();

ko.Listen(iPAddress, configuration.ServerUri.Port, listenOptions =>
{
// Create self-signed cert for server.
using (RSA rsa = RSA.Create())
if (configuration.ServerUri.Scheme == "https")
{
var certReq = new CertificateRequest($"CN={ServerUri.Host}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
certReq.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
certReq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
X509Certificate2 cert = certReq.CreateSelfSigned(DateTimeOffset.UtcNow.AddMonths(-1), DateTimeOffset.UtcNow.AddMonths(1));
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
// Create self-signed cert for server.
using (RSA rsa = RSA.Create())
{
cert = new X509Certificate2(cert.Export(X509ContentType.Pfx));
var certReq = new CertificateRequest($"CN={ServerUri.Host}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
certReq.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
certReq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
X509Certificate2 cert = certReq.CreateSelfSigned(DateTimeOffset.UtcNow.AddMonths(-1), DateTimeOffset.UtcNow.AddMonths(1));
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
cert = new X509Certificate2(cert.Export(X509ContentType.Pfx));
}
listenOptions.UseHttps(cert);
}
listenOptions.UseHttps(cert);
}
else
{
listenOptions.Protocols =
configuration.HttpVersion == new Version(2,0) ?
HttpProtocols.Http2 :
HttpProtocols.Http1 ;
}
});
});
Expand All @@ -95,7 +108,6 @@ public StressServer(Configuration configuration)
// Set up how each request should be handled by the server.
.Configure(app =>
{
var head = new[] { "HEAD" };
app.UseRouting();
app.UseEndpoints(MapRoutes);
});
Expand All @@ -116,6 +128,10 @@ private static void MapRoutes(IEndpointRouteBuilder endpoints)
var head = new[] { "HEAD" };

endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("ok");
});
endpoints.MapGet("/get", async context =>
{
// Get requests just send back the requested content.
string content = CreateResponseContent(context);
Expand All @@ -136,8 +152,7 @@ private static void MapRoutes(IEndpointRouteBuilder endpoints)
{
(string name, StringValues values)[] headersToEcho =
context.Request.Headers
// filter the pseudo-headers surfaced by Kestrel
.Where(h => !h.Key.StartsWith(':'))
.Where(h => h.Key.StartsWith("header-"))
// kestrel does not seem to be splitting comma separated header values, handle here
.Select(h => (h.Key, new StringValues(h.Value.SelectMany(v => v.Split(',')).Select(x => x.Trim()).ToArray())))
.ToArray();
Expand All @@ -147,14 +162,18 @@ private static void MapRoutes(IEndpointRouteBuilder endpoints)
context.Response.Headers.Add(name, values);
}

// send back a checksum of all the echoed headers
uint checksum = ChecksumHelpers.ComputeHeaderChecksum(headersToEcho);
context.Response.Headers.Add("crc32", checksum.ToString());

await context.Response.WriteAsync("ok");

if (context.Response.SupportsTrailers())
{
// just add variations of already echoed headers as trailers
foreach ((string name, StringValues values) in headersToEcho)
{
context.Response.AppendTrailer(name + "-Trailer", values);
context.Response.AppendTrailer(name + "-trailer", values);
}
}

Expand Down Expand Up @@ -196,10 +215,17 @@ private static void MapRoutes(IEndpointRouteBuilder endpoints)
{
// Echos back the requested content in a full duplex manner, but one byte at a time.
var buffer = new byte[1];
uint hashAcc = 0;
while ((await context.Request.Body.ReadAsync(buffer)) != 0)
{
ChecksumHelpers.Append(buffer, ref hashAcc);
await context.Response.Body.WriteAsync(buffer);
}

if (context.Response.SupportsTrailers())
{
context.Response.AppendTrailer("crc32", hashAcc.ToString());
}
});
endpoints.MapMethods("/", head, context =>
{
Expand Down