using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using System.Web;
namespace HttpStress
{
/// <summary>Client context containing information pertaining to a single request.</summary>
public sealed class RequestContext
{
+ private const string alphaNumeric = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
private readonly Random _random;
private readonly HttpClient _client;
private readonly double _cancellationProbability;
}
}
- public string GetRandomString(int maxLength)
+ /// Gets a random ASCII string within specified length range
+ public string GetRandomString(int minLength, int maxLength, bool alphaNumericOnly = true)
{
- int length = _random.Next(0, maxLength);
+ int length = _random.Next(minLength, maxLength);
var sb = new StringBuilder(length);
for (int i = 0; i < length; i++)
{
- sb.Append((char)(_random.Next(0, 26) + 'a'));
+ if (alphaNumericOnly)
+ {
+ // alpha character
+ sb.Append(alphaNumeric[_random.Next(alphaNumeric.Length)]);
+ }
+ else
+ {
+ // use a random ascii character
+ sb.Append((char)_random.Next(0, 128));
+ }
}
+
return sb.ToString();
}
- public string GetRandomSubstring(string input)
+ public byte[] GetRandomBytes(int minBytes, int maxBytes)
{
- int offset = _random.Next(0, input.Length);
- int length = _random.Next(0, input.Length - offset + 1);
- return input.Substring(offset, length);
+ byte[] bytes = new byte[_random.Next(minBytes, maxBytes)];
+ _random.NextBytes(bytes);
+ return bytes;
}
public bool GetRandomBoolean(double probability = 0.5)
return _random.NextDouble() < probability;
}
+ public void PopulateWithRandomHeaders(HttpRequestHeaders headers)
+ {
+ int numHeaders = _random.Next(maxValue: 100);
+
+ for (int i = 0; i < numHeaders; i++)
+ {
+ string name = $"Header-{i}";
+ IEnumerable<string> values = Enumerable.Range(0, _random.Next(0, 5)).Select(_ => HttpUtility.UrlEncode(GetRandomString(0, 30, alphaNumericOnly: false)));
+ headers.Add(name, values);
+ }
+ }
+
public int GetRandomInt32(int minValueInclusive, int maxValueExclusive) => _random.Next(minValueInclusive, maxValueExclusive);
public Version GetRandomHttpVersion() => GetRandomBoolean(_http2Probability) ? new Version(2, 0) : new Version(1, 1);
using (var req = new HttpRequestMessage(HttpMethod.Get, "/") { Version = httpVersion })
using (HttpResponseMessage m = await ctx.SendAsync(req))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
ValidateContent(ctx.ContentSource, await m.Content.ReadAsStringAsync());
}
}),
using (var req = new HttpRequestMessage(HttpMethod.Get, "/slow") { Version = httpVersion })
using (HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
+
using (Stream s = await m.Content.ReadAsStreamAsync())
{
s.ReadByte(); // read single byte from response and throw the rest away
using (var req = new HttpRequestMessage(HttpMethod.Get, "/headers") { Version = httpVersion })
using (HttpResponseMessage m = await ctx.SendAsync(req))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
ValidateContent(ctx.ContentSource, await m.Content.ReadAsStringAsync());
}
}),
+ ("GET Echo Headers",
+ async ctx =>
+ {
+ Version httpVersion = ctx.GetRandomHttpVersion();
+
+ using (var req = new HttpRequestMessage(HttpMethod.Get, "/echoHeaders") { Version = httpVersion })
+ {
+ ctx.PopulateWithRandomHeaders(req.Headers);
+
+ using (HttpResponseMessage res = await ctx.SendAsync(req))
+ {
+ ValidateHttpVersion(res, httpVersion);
+ ValidateStatusCode(res);
+
+ // Validate that response headers are being echoed
+ foreach (var reqHeader in req.Headers)
+ {
+ if (!res.Headers.TryGetValues(reqHeader.Key, out var values))
+ {
+ throw new Exception($"Expected response header name {reqHeader.Key} missing.");
+ }
+ else if (!reqHeader.Value.SequenceEqual(values))
+ {
+ string FmtValues(IEnumerable<string> values) => $"{string.Join(", ", values.Select(x => $"\"{x}\""))}";
+ throw new Exception($"Unexpected values for header {reqHeader.Key}. Expected {FmtValues(reqHeader.Value)} but got {FmtValues(values)}");
+ }
+ }
+ }
+
+ }
+ }),
+
("GET Parameters",
async ctx =>
{
Version httpVersion = ctx.GetRandomHttpVersion();
string uri = "/variables";
- string expectedResponse = GetGetQueryParameters(ref uri, ctx.MaxRequestLineSize, ctx.ContentSource, ctx, ctx.MaxRequestParameters);
+ string expectedResponse = GetGetQueryParameters(ref uri, ctx.MaxRequestLineSize, ctx, ctx.MaxRequestParameters);
using (var req = new HttpRequestMessage(HttpMethod.Get, uri) { Version = httpVersion })
using (HttpResponseMessage m = await ctx.SendAsync(req))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
ValidateContent(expectedResponse, await m.Content.ReadAsStringAsync(), $"Uri: {uri}");
}
}),
("POST",
async ctx =>
{
- string content = ctx.GetRandomSubstring(ctx.ContentSource);
+ string content = ctx.GetRandomString(0, ctx.MaxContentLength);
Version httpVersion = ctx.GetRandomHttpVersion();
using (var req = new HttpRequestMessage(HttpMethod.Post, "/") { Version = httpVersion, Content = new StringDuplexContent(content) })
using (HttpResponseMessage m = await ctx.SendAsync(req))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
ValidateContent(content, await m.Content.ReadAsStringAsync());;
}
}),
("POST Multipart Data",
async ctx =>
{
- (string expected, MultipartContent formDataContent) formData = GetMultipartContent(ctx.ContentSource, ctx, ctx.MaxRequestParameters);
+ (string expected, MultipartContent formDataContent) formData = GetMultipartContent(ctx, ctx.MaxRequestParameters);
Version httpVersion = ctx.GetRandomHttpVersion();
using (var req = new HttpRequestMessage(HttpMethod.Post, "/") { Version = httpVersion, Content = formData.formDataContent })
using (HttpResponseMessage m = await ctx.SendAsync(req))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
ValidateContent($"{formData.expected}", await m.Content.ReadAsStringAsync());;
}
}),
("POST Duplex",
async ctx =>
{
- string content = ctx.GetRandomSubstring(ctx.ContentSource);
+ string content = ctx.GetRandomString(0, ctx.MaxContentLength);
Version httpVersion = ctx.GetRandomHttpVersion();
using (var req = new HttpRequestMessage(HttpMethod.Post, "/duplex") { Version = httpVersion, Content = new StringDuplexContent(content) })
using (HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
ValidateContent(content, await m.Content.ReadAsStringAsync());
}
}),
("POST Duplex Slow",
async ctx =>
{
- string content = ctx.GetRandomSubstring(ctx.ContentSource);
+ string content = ctx.GetRandomString(0, ctx.MaxContentLength);
Version httpVersion = ctx.GetRandomHttpVersion();
using (var req = new HttpRequestMessage(HttpMethod.Post, "/duplexSlow") { Version = httpVersion, Content = new ByteAtATimeNoLengthContent(Encoding.ASCII.GetBytes(content)) })
using (HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
ValidateContent(content, await m.Content.ReadAsStringAsync());
}
}),
("POST ExpectContinue",
async ctx =>
{
- string content = ctx.GetRandomSubstring(ctx.ContentSource);
+ string content = ctx.GetRandomString(0, ctx.MaxContentLength);
Version httpVersion = ctx.GetRandomHttpVersion();
using (var req = new HttpRequestMessage(HttpMethod.Post, "/") { Version = httpVersion, Content = new StringContent(content) })
req.Headers.ExpectContinue = true;
using (HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
ValidateContent(content, await m.Content.ReadAsStringAsync());
}
}
using (var req = new HttpRequestMessage(HttpMethod.Head, "/") { Version = httpVersion })
using (HttpResponseMessage m = await ctx.SendAsync(req))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
+
if (m.Content.Headers.ContentLength != ctx.MaxContentLength)
{
throw new Exception($"Expected {ctx.MaxContentLength}, got {m.Content.Headers.ContentLength}");
("PUT",
async ctx =>
{
- string content = ctx.GetRandomSubstring(ctx.ContentSource);
+ string content = ctx.GetRandomString(0, ctx.MaxContentLength);
Version httpVersion = ctx.GetRandomHttpVersion();
using (var req = new HttpRequestMessage(HttpMethod.Put, "/") { Version = httpVersion, Content = new StringContent(content) })
using (HttpResponseMessage m = await ctx.SendAsync(req))
{
- ValidateResponse(m, httpVersion);
+ ValidateHttpVersion(m, httpVersion);
+ ValidateStatusCode(m);
+
string r = await m.Content.ReadAsStringAsync();
if (r != "") throw new Exception($"Got unexpected response: {r}");
}
};
// Validation of a response message
- private static void ValidateResponse(HttpResponseMessage m, Version expectedVersion)
+ private static void ValidateHttpVersion(HttpResponseMessage m, Version expectedVersion)
{
if (m.Version != expectedVersion)
{
}
}
+ private static void ValidateStatusCode(HttpResponseMessage m, HttpStatusCode expectedStatus = HttpStatusCode.OK)
+ {
+ if (m.StatusCode != expectedStatus)
+ {
+ throw new Exception($"Expected status code {expectedStatus}, got {m.StatusCode}");
+ }
+ }
+
private static void ValidateContent(string expectedContent, string actualContent, string details = null)
{
if (actualContent != expectedContent)
}
}
- private static string GetGetQueryParameters(ref string uri, int maxRequestLineSize, string contentSource, RequestContext clientContext, int numParameters)
+ private static string GetGetQueryParameters(ref string uri, int maxRequestLineSize, RequestContext clientContext, int numParameters)
{
if (maxRequestLineSize < uri.Length)
{
uriSb.Append(key);
- string value = clientContext.GetRandomString(Math.Min(appxMaxValueLength, remainingLength));
+ string value = clientContext.GetRandomString(0, Math.Min(appxMaxValueLength, remainingLength));
expectedString.Append(value);
uriSb.Append(value);
}
return expectedString.ToString();
}
- private static (string, MultipartContent) GetMultipartContent(string contentSource, RequestContext clientContext, int numFormFields)
+ private static (string, MultipartContent) GetMultipartContent(RequestContext clientContext, int numFormFields)
{
- var multipartContent = new MultipartContent("prefix" + clientContext.GetRandomSubstring(contentSource), "test_boundary");
+ var multipartContent = new MultipartContent("prefix" + clientContext.GetRandomString(0, clientContext.MaxContentLength, alphaNumericOnly: true), "test_boundary");
StringBuilder sb = new StringBuilder();
int num = clientContext.GetRandomInt32(1, numFormFields + 1);
for (int i = 0; i < num; i++)
{
sb.Append("--test_boundary\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n");
- string content = clientContext.GetRandomSubstring(contentSource);
+ string content = clientContext.GetRandomString(0, clientContext.MaxContentLength);
sb.Append(content);
sb.Append("\r\n");
multipartContent.Add(new StringContent(content));
[Flags]
enum RunMode { server = 1, client = 2, both = 3 };
- public static void Main(string[] args)
+ public static async Task Main(string[] args)
{
var cmd = new RootCommand();
cmd.AddOption(new Option("-n", "Max number of requests to make concurrently.") { Argument = new Argument<int>("numWorkers", Environment.ProcessorCount) });
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) });
+ cmd.AddOption(new Option("-xops", "Indices of the operations to exclude") { Argument = new Argument<int[]>("space-delimited indices", null) });
cmd.AddOption(new Option("-trace", "Enable Microsoft-System-Net-Http tracing.") { Argument = new Argument<string>("\"console\" or path") });
cmd.AddOption(new Option("-aspnetlog", "Enable ASP.NET warning and error logging.") { Argument = new Argument<bool>("enable", false) });
cmd.AddOption(new Option("-listOps", "List available options.") { Argument = new Argument<bool>("enable", false) });
return;
}
+ await
Run(
runMode : cmdline.ValueForOption<RunMode>("-runMode"),
serverUri : cmdline.ValueForOption<Uri>("-serverUri"),
httpVersion : cmdline.ValueForOption<Version>("-http"),
connectionLifetime : cmdline.ValueForOption<int?>("-connectionLifetime"),
opIndices : cmdline.ValueForOption<int[]>("-ops"),
+ excludedOpIndices : cmdline.ValueForOption<int[]>("-xops"),
logPath : cmdline.HasOption("-trace") ? cmdline.ValueForOption<string>("-trace") : null,
aspnetLog : cmdline.ValueForOption<bool>("-aspnetlog"),
listOps : cmdline.ValueForOption<bool>("-listOps"),
displayIntervalSeconds : cmdline.ValueForOption<int>("-displayInterval"));
}
- private static void Run(RunMode runMode, Uri serverUri, bool httpSys, int concurrentRequests, int maxContentLength, int maxRequestLineSize, Version httpVersion, int? connectionLifetime, int[] opIndices, string logPath, bool aspnetLog, bool listOps, int seed, int numParameters, double cancellationProbability, int displayIntervalSeconds)
+ private static async Task Run(RunMode runMode, Uri serverUri, bool httpSys, int concurrentRequests, int maxContentLength, int maxRequestLineSize, Version httpVersion, int? connectionLifetime, int[] opIndices, int[] excludedOpIndices, string logPath, bool aspnetLog, bool listOps, int seed, int numParameters, double cancellationProbability, int displayIntervalSeconds)
{
- if (serverUri.Scheme != "https")
+ (string name, Func<RequestContext, Task> op)[] clientOperations = ClientOperations.Operations;
+
+ // handle operation index arguments
+ switch (opIndices, excludedOpIndices)
{
- Console.Error.WriteLine("Server uri must be https.");
- return;
- }
+ case (null, null):
+ break;
+ case (_, null):
+ clientOperations = opIndices.Select(i => clientOperations[i]).ToArray();
+ break;
+ case (null, _):
+ opIndices =
+ Enumerable
+ .Range(0, clientOperations.Length)
+ .Concat(excludedOpIndices)
+ .GroupBy(x => x)
+ .Where(gp => gp.Count() < 2)
+ .Select(gp => gp.Key)
+ .ToArray();
- (string name, Func<RequestContext, Task> op)[] clientOperations = ClientOperations.Operations;
+ clientOperations = opIndices.Select(i => clientOperations[i]).ToArray();
+ break;
+ default:
+ Console.Error.WriteLine("Cannot specify both -ops and -xops flags simultaneously");
+ return;
+ }
Console.WriteLine(" .NET Core: " + Path.GetFileName(Path.GetDirectoryName(typeof(object).Assembly.Location)));
Console.WriteLine(" ASP.NET Core: " + Path.GetFileName(Path.GetDirectoryName(typeof(WebHost).Assembly.Location)));
return;
}
+ if (serverUri.Scheme != "https")
+ {
+ Console.Error.WriteLine("Server uri must be https.");
+ return;
+ }
+
if (runMode.HasFlag(RunMode.server))
{
// Start the Kestrel web server in-proc.
{
// Start the client.
Console.WriteLine($"Starting {concurrentRequests} client workers.");
- if (opIndices != null)
- {
- clientOperations = opIndices.Select(i => clientOperations[i]).ToArray();
- }
new StressClient(
serverUri: serverUri,
displayInterval: TimeSpan.FromSeconds(displayIntervalSeconds));
}
- AwaitCancelKeyPress().Wait();
+ await AwaitCancelKeyPress();
}
private static async Task AwaitCancelKeyPress()