From a41bf6228f3c6ac581c774a1ffd4e5e8d651eadb Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 8 Mar 2018 10:25:12 +1300 Subject: [PATCH] Client sends user agent with version based on assembly version (#1551) --- .../Constants.cs | 29 +++++++++-- .../WebSocketsTransport.cs | 3 ++ .../LongPollingTransportTests.cs | 48 +++++++++++++++++++ .../ServerSentEventsTransportTests.cs | 39 +++++++++++++++ .../EchoEndPoint.cs | 2 +- .../HttpHeaderEndPoint.cs | 37 ++++++++++++++ .../Startup.cs | 2 + .../WebSocketsTransportTests.cs | 34 +++++++++++++ 8 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 test/Microsoft.AspNetCore.SignalR.Tests/HttpHeaderEndPoint.cs diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/Constants.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/Constants.cs index 590ac11206..ad61ab3f9e 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/Constants.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/Constants.cs @@ -1,13 +1,36 @@ // 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.Diagnostics; +using System.Linq; using System.Net.Http.Headers; +using System.Reflection; namespace Microsoft.AspNetCore.Sockets.Client.Http { - public class Constants + public static class Constants { - private static readonly string UserAgent = "Microsoft.AspNetCore.Sockets.Client.Http/1.0.0-alpha"; - public static readonly ProductInfoHeaderValue UserAgentHeader = ProductInfoHeaderValue.Parse(UserAgent); + public static readonly ProductInfoHeaderValue UserAgentHeader; + + static Constants() + { + var userAgent = "Microsoft.AspNetCore.Sockets.Client.Http"; + + var assemblyVersion = typeof(Constants) + .Assembly + .GetCustomAttributes() + .FirstOrDefault(); + + Debug.Assert(assemblyVersion != null); + + // assembly version attribute should always be present + // but in case it isn't then don't include version in user-agent + if (assemblyVersion != null) + { + userAgent += "/" + assemblyVersion.InformationalVersion; + } + + UserAgentHeader = ProductInfoHeaderValue.Parse(userAgent); + } } } diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.cs index 353720319e..0837e138b9 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.cs @@ -34,6 +34,9 @@ namespace Microsoft.AspNetCore.Sockets.Client { _webSocket = new ClientWebSocket(); + // Issue in ClientWebSocket prevents user-agent being set - https://github.com/dotnet/corefx/issues/26627 + //_webSocket.Options.SetRequestHeader("User-Agent", Constants.UserAgentHeader.ToString()); + if (httpOptions?.Headers != null) { foreach (var header in httpOptions.Headers) diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/LongPollingTransportTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/LongPollingTransportTests.cs index 2b319b3f74..fafe9c1945 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/LongPollingTransportTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/LongPollingTransportTests.cs @@ -4,8 +4,11 @@ using System; using System.Collections.Generic; using System.IO.Pipelines; +using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Channels; @@ -13,6 +16,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client.Tests; using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Client; +using Microsoft.AspNetCore.Sockets.Client.Http; using Moq; using Moq.Protected; using Xunit; @@ -388,6 +392,50 @@ namespace Microsoft.AspNetCore.Client.Tests } } + [Fact] + public async Task LongPollingTransportSetsUserAgent() + { + HttpHeaderValueCollection userAgentHeaderCollection = null; + + var mockHttpHandler = new Mock(); + mockHttpHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Returns(async (request, cancellationToken) => + { + userAgentHeaderCollection = request.Headers.UserAgent; + await Task.Yield(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK); + }); + + using (var httpClient = new HttpClient(mockHttpHandler.Object)) + { + var longPollingTransport = new LongPollingTransport(httpClient); + + try + { + var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); + + Assert.Null(longPollingTransport.Mode); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: new TestConnection()); + } + finally + { + await longPollingTransport.StopAsync(); + } + } + + Assert.NotNull(userAgentHeaderCollection); + var userAgentHeader = Assert.Single(userAgentHeaderCollection); + Assert.Equal("Microsoft.AspNetCore.Sockets.Client.Http", userAgentHeader.Product.Name); + + // user agent version should come from version embedded in assembly metadata + var assemblyVersion = typeof(Constants) + .Assembly + .GetCustomAttribute(); + + Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version); + } + [Fact] public async Task LongPollingTransportThrowsForInvalidTransferMode() { diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs index 22a89d5e3e..0bd8dc9ee5 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs @@ -4,8 +4,10 @@ using System; using System.IO; using System.IO.Pipelines; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Channels; @@ -13,6 +15,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Client.Tests; using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Client; +using Microsoft.AspNetCore.Sockets.Client.Http; using Moq; using Moq.Protected; using Xunit; @@ -299,6 +302,42 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + [Fact] + public async Task SSETransportSetsUserAgent() + { + HttpHeaderValueCollection userAgentHeaderCollection = null; + + var mockHttpHandler = new Mock(); + mockHttpHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Returns(async (request, cancellationToken) => + { + userAgentHeaderCollection = request.Headers.UserAgent; + await Task.Yield(); + return new HttpResponseMessage { Content = new StringContent(string.Empty) }; + }); + + using (var httpClient = new HttpClient(mockHttpHandler.Object)) + { + var sseTransport = new ServerSentEventsTransport(httpClient); + + var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); + await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of()).OrTimeout(); + await sseTransport.StopAsync().OrTimeout(); + } + + Assert.NotNull(userAgentHeaderCollection); + var userAgentHeader = Assert.Single(userAgentHeaderCollection); + Assert.Equal("Microsoft.AspNetCore.Sockets.Client.Http", userAgentHeader.Product.Name); + + // user agent version should come from version embedded in assembly metadata + var assemblyVersion = typeof(Constants) + .Assembly + .GetCustomAttribute(); + + Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version); + } + [Fact] public async Task SSETransportThrowsForInvalidTransferMode() { diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/EchoEndPoint.cs b/test/Microsoft.AspNetCore.SignalR.Tests/EchoEndPoint.cs index 58e1a9ff6d..b7bedecc60 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/EchoEndPoint.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/EchoEndPoint.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { public class EchoEndPoint : EndPoint { - public async override Task OnConnectedAsync(ConnectionContext connection) + public override async Task OnConnectedAsync(ConnectionContext connection) { var result = await connection.Transport.Input.ReadAsync(); var buffer = result.Buffer; diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HttpHeaderEndPoint.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HttpHeaderEndPoint.cs new file mode 100644 index 0000000000..949260c2ae --- /dev/null +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HttpHeaderEndPoint.cs @@ -0,0 +1,37 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Protocols; +using Microsoft.AspNetCore.Sockets; +using Microsoft.AspNetCore.Sockets.Http.Features; + +namespace Microsoft.AspNetCore.SignalR.Tests +{ + public class HttpHeaderEndPoint : EndPoint + { + public override async Task OnConnectedAsync(ConnectionContext connection) + { + var result = await connection.Transport.Input.ReadAsync(); + var buffer = result.Buffer; + + try + { + var headers = connection.Features.Get().HttpContext.Request.Headers; + + var headerName = Encoding.UTF8.GetString(buffer.ToArray()); + var headerValues = headers.FirstOrDefault(h => string.Equals(h.Key, headerName, StringComparison.OrdinalIgnoreCase)).Value.ToArray(); + + var data = Encoding.UTF8.GetBytes(string.Join(",", headerValues)); + + await connection.Transport.Output.WriteAsync(data); + } + finally + { + connection.Transport.Input.AdvanceTo(buffer.End); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/Startup.cs b/test/Microsoft.AspNetCore.SignalR.Tests/Startup.cs index 714d3602dc..eba9d19966 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/Startup.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/Startup.cs @@ -14,11 +14,13 @@ namespace Microsoft.AspNetCore.SignalR.Tests services.AddSockets(); services.AddSignalR(); services.AddEndPoint(); + services.AddEndPoint(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseSockets(options => options.MapEndPoint("/echo")); + app.UseSockets(options => options.MapEndPoint("/httpheader")); app.UseSignalR(options => options.MapHub("/uncreatable")); } } diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/WebSocketsTransportTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/WebSocketsTransportTests.cs index cbfb2994a3..a2996c4896 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/WebSocketsTransportTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/WebSocketsTransportTests.cs @@ -4,12 +4,17 @@ using System; using System.Buffers; using System.IO.Pipelines; +using System.Linq; +using System.Reflection; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Client; +using Microsoft.AspNetCore.Sockets.Client.Http; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.Logging.Testing; using Moq; +using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; @@ -45,6 +50,35 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [ConditionalFact(Skip = "Issue in ClientWebSocket prevents user-agent being set - https://github.com/dotnet/corefx/issues/26627")] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")] + public async Task WebSocketsTransportSendsUserAgent() + { + using (StartLog(out var loggerFactory)) + { + var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); + var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); + await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/httpheader"), pair.Application, + TransferMode.Binary, connection: Mock.Of()).OrTimeout(); + + await pair.Transport.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(pair.Transport.Input.TryRead(out var result)); + + string 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.Sockets.Client.Http/" + assemblyVersion.InformationalVersion, userAgent); + } + } + [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")] public async Task WebSocketsTransportStopsWhenConnectionChannelClosed()