using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using Xunit; namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn { public class AutobahnTester : IDisposable { private readonly List _deployers = new List(); private readonly List _deployments = new List(); private readonly List _expectations = new List(); private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; public AutobahnSpec Spec { get; } public AutobahnTester(ILoggerFactory loggerFactory, AutobahnSpec baseSpec) { _loggerFactory = loggerFactory; _logger = _loggerFactory.CreateLogger("AutobahnTester"); Spec = baseSpec; } public async Task Run(CancellationToken cancellationToken) { var specFile = Path.GetTempFileName(); try { // Start pinging the servers to see that they're still running var pingCts = new CancellationTokenSource(); var pinger = new Timer(state => Pinger((CancellationToken)state), pingCts.Token, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); Spec.WriteJson(specFile); // Run the test (write something to the console so people know this will take a while...) _logger.LogInformation("Now launching Autobahn Test Suite. This will take a while."); var exitCode = await Wstest.Default.ExecAsync("-m fuzzingclient -s " + specFile, cancellationToken, _loggerFactory.CreateLogger("wstest")); if (exitCode != 0) { throw new Exception("wstest failed"); } pingCts.Cancel(); } finally { if (File.Exists(specFile)) { File.Delete(specFile); } } cancellationToken.ThrowIfCancellationRequested(); // Parse the output. var outputFile = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", Spec.OutputDirectory, "index.json"); using (var reader = new StreamReader(File.OpenRead(outputFile))) { return AutobahnResult.FromReportJson(JObject.Parse(await reader.ReadToEndAsync())); } } // Async void! It's OK here because we are running in a timer. We're just using async void to chain continuations. // There's nobody to await this, hence async void. private async void Pinger(CancellationToken token) { try { while (!token.IsCancellationRequested) { try { foreach (var deployment in _deployments) { if (token.IsCancellationRequested) { return; } var resp = await deployment.HttpClient.GetAsync("/ping", token); if (!resp.IsSuccessStatusCode) { _logger.LogWarning("Non-successful response when pinging {url}: {statusCode} {reasonPhrase}", deployment.ApplicationBaseUri, resp.StatusCode, resp.ReasonPhrase); } } } catch (OperationCanceledException) { // We don't want to throw when the token fires, just stop. } } } catch (Exception ex) { _logger.LogError(ex, "Error while pinging servers"); } } public void Verify(AutobahnResult result) { var failures = new StringBuilder(); foreach (var serverResult in result.Servers) { var serverExpectation = _expectations.FirstOrDefault(e => e.Server == serverResult.Server && e.Ssl == serverResult.Ssl); if (serverExpectation == null) { failures.AppendLine($"Expected no results for server: {serverResult.Name} but found results!"); } else { serverExpectation.Verify(serverResult, failures); } } Assert.True(failures.Length == 0, "Autobahn results did not meet expectations:" + Environment.NewLine + failures.ToString()); } public async Task DeployTestAndAddToSpec(ServerType server, bool ssl, string environment, CancellationToken cancellationToken, Action expectationConfig = null) { var baseUrl = ssl ? "https://localhost:0" : "http://localhost:0"; var sslNamePart = ssl ? "SSL" : "NoSSL"; var name = $"{server}|{sslNamePart}|{environment}"; var logger = _loggerFactory.CreateLogger($"AutobahnTestApp:{server}:{sslNamePart}:{environment}"); var appPath = Helpers.GetApplicationPath("AutobahnTestApp"); var configPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "Http.config"); var targetFramework = #if NETCOREAPP2_1 "netcoreapp2.1"; #elif NETCOREAPP2_0 "netcoreapp2.0"; #else #error Target frameworks need to be updated #endif var parameters = new DeploymentParameters(appPath, server, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) { ApplicationBaseUriHint = baseUrl, ApplicationType = ApplicationType.Portable, TargetFramework = targetFramework, EnvironmentName = environment, SiteName = "HttpTestSite", // This is configured in the Http.config ServerConfigTemplateContent = (server == ServerType.IISExpress) ? File.ReadAllText(configPath) : null, }; var deployer = ApplicationDeployerFactory.Create(parameters, _loggerFactory); var result = await deployer.DeployAsync(); _deployers.Add(deployer); _deployments.Add(result); cancellationToken.ThrowIfCancellationRequested(); var handler = new HttpClientHandler(); if (ssl) { // Don't take this out of the "if(ssl)". If we set it on some platforms, it crashes // So we avoid running SSL tests on those platforms (for now). // See https://github.com/dotnet/corefx/issues/9728 handler.ServerCertificateCustomValidationCallback = (_, __, ___, ____) => true; } var client = result.CreateHttpClient(handler); // Make sure the server works var resp = await RetryHelper.RetryRequest(() => { cancellationToken.ThrowIfCancellationRequested(); return client.GetAsync(result.ApplicationBaseUri); }, logger, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, result.HostShutdownToken).Token); resp.EnsureSuccessStatusCode(); cancellationToken.ThrowIfCancellationRequested(); // Add to the current spec var wsUrl = result.ApplicationBaseUri.Replace("https://", "wss://").Replace("http://", "ws://"); Spec.WithServer(name, wsUrl); var expectations = new AutobahnExpectations(server, ssl, environment); expectationConfig?.Invoke(expectations); _expectations.Add(expectations); cancellationToken.ThrowIfCancellationRequested(); } public void Dispose() { foreach (var deployer in _deployers) { deployer.Dispose(); } } } }