// 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.Buffers; using System.Net; using System.Net.WebSockets; using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Connections.Client; using Microsoft.AspNetCore.Http.Connections.Client.Internal; using Microsoft.AspNetCore.Testing.xunit; using Moq; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.SignalR.Tests { [Collection(EndToEndTestsCollection.Name)] public class WebSocketsTransportTests : VerifiableServerLoggedTest { public WebSocketsTransportTests(ServerFixture serverFixture, ITestOutputHelper output) : base(serverFixture, output) { } [ConditionalFact] [WebSocketsSupportedCondition] public void HttpOptionsSetOntoWebSocketOptions() { ClientWebSocketOptions webSocketsOptions = null; var httpOptions = new HttpConnectionOptions(); httpOptions.Cookies.Add(new Cookie("Name", "Value", string.Empty, "fakeuri.org")); var clientCertificate = new X509Certificate(); httpOptions.ClientCertificates.Add(clientCertificate); httpOptions.UseDefaultCredentials = false; httpOptions.Credentials = Mock.Of(); httpOptions.Proxy = Mock.Of(); httpOptions.WebSocketConfiguration = options => webSocketsOptions = options; var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: httpOptions, loggerFactory: null, accessTokenProvider: null); Assert.NotNull(webSocketsTransport); Assert.NotNull(webSocketsOptions); Assert.Equal(1, webSocketsOptions.Cookies.Count); Assert.Single(webSocketsOptions.ClientCertificates); Assert.Same(clientCertificate, webSocketsOptions.ClientCertificates[0]); Assert.False(webSocketsOptions.UseDefaultCredentials); Assert.Same(httpOptions.Proxy, webSocketsOptions.Proxy); Assert.Same(httpOptions.Credentials, webSocketsOptions.Credentials); } [ConditionalFact] [WebSocketsSupportedCondition] public async Task WebSocketsTransportStopsSendAndReceiveLoopsWhenTransportIsStopped() { using (StartVerifiableLog(out var loggerFactory)) { var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: loggerFactory, accessTokenProvider: null); await webSocketsTransport.StartAsync(new Uri(ServerFixture.WebSocketsUrl + "/echo"), TransferFormat.Binary).OrTimeout(); await webSocketsTransport.StopAsync().OrTimeout(); await webSocketsTransport.Running.OrTimeout(); } } [ConditionalFact(Skip = "Issue in ClientWebSocket prevents user-agent being set - https://github.com/dotnet/corefx/issues/26627")] [WebSocketsSupportedCondition] public async Task WebSocketsTransportSendsUserAgent() { using (StartVerifiableLog(out var loggerFactory)) { var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: loggerFactory, accessTokenProvider: null); await webSocketsTransport.StartAsync(new Uri(ServerFixture.WebSocketsUrl + "/httpheader"), TransferFormat.Binary).OrTimeout(); await webSocketsTransport.Output.WriteAsync(Encoding.UTF8.GetBytes("User-Agent")); // The HTTP header endpoint closes the connection immediately after sending response which should stop the transport await webSocketsTransport.Running.OrTimeout(); Assert.True(webSocketsTransport.Input.TryRead(out var result)); var userAgent = Encoding.UTF8.GetString(result.Buffer.ToArray()); // user agent version should come from version embedded in assembly metadata var assemblyVersion = typeof(Constants) .Assembly .GetCustomAttribute(); Assert.Equal("Microsoft.AspNetCore.Http.Connections.Client/" + assemblyVersion.InformationalVersion, userAgent); } } [ConditionalFact] [WebSocketsSupportedCondition] public async Task WebSocketsTransportSendsXRequestedWithHeader() { using (StartVerifiableLog(out var loggerFactory)) { var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: loggerFactory, accessTokenProvider: null); await webSocketsTransport.StartAsync(new Uri(ServerFixture.WebSocketsUrl + "/httpheader"), TransferFormat.Binary).OrTimeout(); await webSocketsTransport.Output.WriteAsync(Encoding.UTF8.GetBytes("X-Requested-With")); // The HTTP header endpoint closes the connection immediately after sending response which should stop the transport await webSocketsTransport.Running.OrTimeout(); Assert.True(webSocketsTransport.Input.TryRead(out var result)); var headerValue = Encoding.UTF8.GetString(result.Buffer.ToArray()); Assert.Equal("XMLHttpRequest", headerValue); } } [ConditionalFact] [WebSocketsSupportedCondition] public async Task WebSocketsTransportStopsWhenConnectionChannelClosed() { using (StartVerifiableLog(out var loggerFactory)) { var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: loggerFactory, accessTokenProvider: null); await webSocketsTransport.StartAsync(new Uri(ServerFixture.WebSocketsUrl + "/echo"), TransferFormat.Binary); webSocketsTransport.Output.Complete(); await webSocketsTransport.Running.OrTimeout(TimeSpan.FromSeconds(10)); } } [ConditionalTheory] [WebSocketsSupportedCondition] [InlineData(TransferFormat.Text)] [InlineData(TransferFormat.Binary)] public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferFormat transferFormat) { using (StartVerifiableLog(out var loggerFactory)) { var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: loggerFactory, accessTokenProvider: null); await webSocketsTransport.StartAsync(new Uri(ServerFixture.WebSocketsUrl + "/echoAndClose"), transferFormat); await webSocketsTransport.Output.WriteAsync(new byte[] { 0x42 }); // The echoAndClose endpoint closes the connection immediately after sending response which should stop the transport await webSocketsTransport.Running.OrTimeout(); Assert.True(webSocketsTransport.Input.TryRead(out var result)); Assert.Equal(new byte[] { 0x42 }, result.Buffer.ToArray()); webSocketsTransport.Input.AdvanceTo(result.Buffer.End); } } [ConditionalTheory] [WebSocketsSupportedCondition] [InlineData(TransferFormat.Text)] [InlineData(TransferFormat.Binary)] public async Task WebSocketsTransportSetsTransferFormat(TransferFormat transferFormat) { using (StartVerifiableLog(out var loggerFactory)) { var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: loggerFactory, accessTokenProvider: null); await webSocketsTransport.StartAsync(new Uri(ServerFixture.WebSocketsUrl + "/echo"), transferFormat).OrTimeout(); await webSocketsTransport.StopAsync().OrTimeout(); await webSocketsTransport.Running.OrTimeout(); } } [ConditionalTheory] [InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed [InlineData((TransferFormat)42)] // Unexpected value [WebSocketsSupportedCondition] public async Task WebSocketsTransportThrowsForInvalidTransferFormat(TransferFormat transferFormat) { using (StartVerifiableLog(out var loggerFactory)) { var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: loggerFactory, accessTokenProvider: null); var exception = await Assert.ThrowsAsync(() => webSocketsTransport.StartAsync(new Uri("http://fakeuri.org"), transferFormat)); Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message); Assert.Equal("transferFormat", exception.ParamName); } } } }