Skip to content

Commit 1256325

Browse files
committed
Merge in 'release/7.0' changes
2 parents 1c6bd93 + 25534d3 commit 1256325

File tree

10 files changed

+196
-34
lines changed

10 files changed

+196
-34
lines changed

src/SignalR/SignalR.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj",
3434
"src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj",
3535
"src\\Security\\Authentication\\JwtBearer\\src\\Microsoft.AspNetCore.Authentication.JwtBearer.csproj",
36+
"src\\Security\\Authentication\\Negotiate\\src\\Microsoft.AspNetCore.Authentication.Negotiate.csproj",
3637
"src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj",
3738
"src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj",
3839
"src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",

src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,6 +1789,110 @@ public async Task WebSocketsCanConnectOverHttp2()
17891789
Assert.Contains(TestSink.Writes, context => context.Message.Contains("Request finished HTTP/2 CONNECT"));
17901790
}
17911791

1792+
[ConditionalTheory]
1793+
[MemberData(nameof(TransportTypes))]
1794+
// Negotiate auth on non-windows requires a lot of setup which is out of scope for these tests
1795+
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux)]
1796+
public async Task TransportFallsbackFromHttp2WhenUsingCredentials(HttpTransportType httpTransportType)
1797+
{
1798+
await using (var server = await StartServer<Startup>(configureKestrelServerOptions: o =>
1799+
{
1800+
o.ConfigureEndpointDefaults(o2 =>
1801+
{
1802+
o2.Protocols = Server.Kestrel.Core.HttpProtocols.Http1;
1803+
o2.UseHttps();
1804+
});
1805+
o.ConfigureHttpsDefaults(httpsOptions =>
1806+
{
1807+
httpsOptions.ServerCertificate = TestCertificateHelper.GetTestCert();
1808+
});
1809+
}))
1810+
{
1811+
var hubConnection = new HubConnectionBuilder()
1812+
.WithLoggerFactory(LoggerFactory)
1813+
.WithUrl(server.Url + "/windowsauthhub", httpTransportType, options =>
1814+
{
1815+
options.HttpMessageHandlerFactory = h =>
1816+
{
1817+
((HttpClientHandler)h).ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
1818+
return h;
1819+
};
1820+
options.WebSocketConfiguration = o =>
1821+
{
1822+
o.RemoteCertificateValidationCallback = (_, _, _, _) => true;
1823+
o.HttpVersion = HttpVersion.Version20;
1824+
};
1825+
options.UseDefaultCredentials = true;
1826+
})
1827+
.Build();
1828+
try
1829+
{
1830+
await hubConnection.StartAsync().DefaultTimeout();
1831+
var echoResponse = await hubConnection.InvokeAsync<string>(nameof(HubWithAuthorization2.Echo), "Foo").DefaultTimeout();
1832+
Assert.Equal("Foo", echoResponse);
1833+
}
1834+
catch (Exception ex)
1835+
{
1836+
LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
1837+
throw;
1838+
}
1839+
finally
1840+
{
1841+
await hubConnection.DisposeAsync().DefaultTimeout();
1842+
}
1843+
}
1844+
1845+
// Check that HTTP/1.1 was used instead of the configured HTTP/2 since Windows Auth is being used
1846+
Assert.Contains(TestSink.Writes, context => context.Message.Contains("Request starting HTTP/1.1 POST"));
1847+
Assert.Contains(TestSink.Writes, context => context.Message.Contains("Request starting HTTP/1.1 GET"));
1848+
Assert.Contains(TestSink.Writes, context => context.Message.Contains("Request finished HTTP/1.1 GET"));
1849+
}
1850+
1851+
[ConditionalFact]
1852+
[WebSocketsSupportedCondition]
1853+
// Negotiate auth on non-windows requires a lot of setup which is out of scope for these tests
1854+
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux)]
1855+
public async Task WebSocketsFailsWhenHttp1NotAllowedAndUsingCredentials()
1856+
{
1857+
await using (var server = await StartServer<Startup>(context => context.EventId.Name == "ErrorStartingTransport",
1858+
configureKestrelServerOptions: o =>
1859+
{
1860+
o.ConfigureEndpointDefaults(o2 =>
1861+
{
1862+
o2.Protocols = Server.Kestrel.Core.HttpProtocols.Http1AndHttp2;
1863+
o2.UseHttps();
1864+
});
1865+
o.ConfigureHttpsDefaults(httpsOptions =>
1866+
{
1867+
httpsOptions.ServerCertificate = TestCertificateHelper.GetTestCert();
1868+
});
1869+
}))
1870+
{
1871+
var hubConnection = new HubConnectionBuilder()
1872+
.WithLoggerFactory(LoggerFactory)
1873+
.WithUrl(server.Url + "/windowsauthhub", HttpTransportType.WebSockets, options =>
1874+
{
1875+
options.HttpMessageHandlerFactory = h =>
1876+
{
1877+
((HttpClientHandler)h).ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
1878+
return h;
1879+
};
1880+
options.WebSocketConfiguration = o =>
1881+
{
1882+
o.RemoteCertificateValidationCallback = (_, _, _, _) => true;
1883+
o.HttpVersion = HttpVersion.Version20;
1884+
o.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;
1885+
};
1886+
options.UseDefaultCredentials = true;
1887+
})
1888+
.Build();
1889+
1890+
var ex = await Assert.ThrowsAsync<AggregateException>(() => hubConnection.StartAsync().DefaultTimeout());
1891+
Assert.Contains("Negotiate Authentication doesn't work with HTTP/2 or higher.", ex.Message);
1892+
await hubConnection.DisposeAsync().DefaultTimeout();
1893+
}
1894+
}
1895+
17921896
[ConditionalFact]
17931897
[WebSocketsSupportedCondition]
17941898
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "HTTP/2 over TLS is not supported on macOS due to missing ALPN support.")]

src/SignalR/clients/csharp/Client/test/FunctionalTests/Microsoft.AspNetCore.SignalR.Client.FunctionalTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<ProjectReference Include="$(SignalRTestUtilsProject)" />
88

99
<Reference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
10+
<Reference Include="Microsoft.AspNetCore.Authentication.Negotiate" />
1011
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
1112
<Reference Include="Microsoft.AspNetCore.Http" />
1213
<Reference Include="Microsoft.AspNetCore.SignalR.Client" />

src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Security.Claims;
88
using Microsoft.AspNetCore.Authentication.JwtBearer;
9+
using Microsoft.AspNetCore.Authentication.Negotiate;
910
using Microsoft.AspNetCore.Authorization;
1011
using Microsoft.AspNetCore.Builder;
1112
using Microsoft.AspNetCore.DataProtection;
@@ -36,6 +37,11 @@ public void ConfigureServices(IServiceCollection services)
3637
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
3738
policy.RequireClaim(ClaimTypes.NameIdentifier);
3839
});
40+
options.AddPolicy(NegotiateDefaults.AuthenticationScheme, policy =>
41+
{
42+
policy.AddAuthenticationSchemes(NegotiateDefaults.AuthenticationScheme);
43+
policy.RequireClaim(ClaimTypes.Name);
44+
});
3945
});
4046

4147
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
@@ -51,6 +57,7 @@ public void ConfigureServices(IServiceCollection services)
5157
IssuerSigningKey = SecurityKey
5258
};
5359
});
60+
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();
5461

5562
// Since tests run in parallel, it's possible multiple servers will startup and read files being written by another test
5663
// Use a unique directory per server to avoid this collision
@@ -85,6 +92,8 @@ public void Configure(IApplicationBuilder app)
8592
endpoints.MapHub<HubWithAuthorization>("/authorizedhub");
8693
endpoints.MapHub<HubWithAuthorization2>("/authorizedhub2")
8794
.RequireAuthorization(new AuthorizeAttribute(JwtBearerDefaults.AuthenticationScheme));
95+
endpoints.MapHub<HubWithAuthorization2>("/windowsauthhub")
96+
.RequireAuthorization(new AuthorizeAttribute(NegotiateDefaults.AuthenticationScheme));
8897

8998
endpoints.MapHub<TestHub>("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents);
9099

src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -462,10 +462,6 @@ private async Task<NegotiationResponse> NegotiateAsync(Uri url, HttpClient httpC
462462

463463
using (var request = new HttpRequestMessage(HttpMethod.Post, uri))
464464
{
465-
#if NETSTANDARD2_1_OR_GREATER || NET7_0_OR_GREATER
466-
// HttpClient gracefully falls back to HTTP/1.1, so it's fine to set the preferred version to a higher version
467-
request.Version = HttpVersion.Version20;
468-
#endif
469465
#if NET5_0_OR_GREATER
470466
request.Options.Set(new HttpRequestOptionsKey<bool>("IsNegotiate"), true);
471467
#else
@@ -542,6 +538,7 @@ private HttpClient CreateHttpClient()
542538
HttpMessageHandler httpMessageHandler = httpClientHandler;
543539

544540
var isBrowser = OperatingSystem.IsBrowser();
541+
var allowHttp2 = true;
545542

546543
if (_httpConnectionOptions != null)
547544
{
@@ -579,11 +576,17 @@ private HttpClient CreateHttpClient()
579576
if (_httpConnectionOptions.UseDefaultCredentials != null)
580577
{
581578
httpClientHandler.UseDefaultCredentials = _httpConnectionOptions.UseDefaultCredentials.Value;
579+
// Negotiate Auth isn't supported over HTTP/2 and HttpClient does not gracefully fallback to HTTP/1.1 in that case
580+
// https://github.com/dotnet/runtime/issues/1582
581+
allowHttp2 = !_httpConnectionOptions.UseDefaultCredentials.Value;
582582
}
583583

584584
if (_httpConnectionOptions.Credentials != null)
585585
{
586586
httpClientHandler.Credentials = _httpConnectionOptions.Credentials;
587+
// Negotiate Auth isn't supported over HTTP/2 and HttpClient does not gracefully fallback to HTTP/1.1 in that case
588+
// https://github.com/dotnet/runtime/issues/1582
589+
allowHttp2 = false;
587590
}
588591
}
589592

@@ -604,6 +607,11 @@ private HttpClient CreateHttpClient()
604607
// Wrap message handler after HttpMessageHandlerFactory to ensure not overridden
605608
httpMessageHandler = new LoggingHttpMessageHandler(httpMessageHandler, _loggerFactory);
606609

610+
if (allowHttp2)
611+
{
612+
httpMessageHandler = new Http2HttpMessageHandler(httpMessageHandler);
613+
}
614+
607615
var httpClient = new HttpClient(httpMessageHandler);
608616
httpClient.Timeout = HttpClientTimeout;
609617

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Runtime.InteropServices;
10+
using System.Text;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
14+
namespace Microsoft.AspNetCore.Http.Connections.Client.Internal;
15+
16+
internal class Http2HttpMessageHandler : DelegatingHandler
17+
{
18+
public Http2HttpMessageHandler(HttpMessageHandler inner) : base(inner) { }
19+
20+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
21+
{
22+
#if NETSTANDARD2_1_OR_GREATER || NET7_0_OR_GREATER
23+
// Check just in case HttpRequestMessage defaults to 3 or higher for some reason
24+
if (request.Version == HttpVersion.Version11)
25+
{
26+
// HttpClient gracefully falls back to HTTP/1.1,
27+
// so it's fine to set the preferred version to a higher version
28+
request.Version = HttpVersion.Version20;
29+
}
30+
#endif
31+
32+
return base.SendAsync(request, cancellationToken);
33+
}
34+
}

src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/LongPollingTransport.cs

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,7 @@ public async Task StartAsync(Uri url, TransferFormat transferFormat, Cancellatio
5050

5151
// Make initial long polling request
5252
// Server uses first long polling request to finish initializing connection and it returns without data
53-
var request = new HttpRequestMessage(HttpMethod.Get, url)
54-
{
55-
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
56-
Version = HttpVersion.Version20,
57-
#endif
58-
};
53+
var request = new HttpRequestMessage(HttpMethod.Get, url);
5954
using (var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false))
6055
{
6156
response.EnsureSuccessStatusCode();
@@ -153,12 +148,7 @@ private async Task Poll(Uri pollUrl, CancellationToken cancellationToken)
153148
{
154149
while (!cancellationToken.IsCancellationRequested)
155150
{
156-
var request = new HttpRequestMessage(HttpMethod.Get, pollUrl)
157-
{
158-
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
159-
Version = HttpVersion.Version20,
160-
#endif
161-
};
151+
var request = new HttpRequestMessage(HttpMethod.Get, pollUrl);
162152

163153
HttpResponseMessage response;
164154

@@ -237,12 +227,7 @@ private async Task SendDeleteRequest(Uri url)
237227
try
238228
{
239229
Log.SendingDeleteRequest(_logger, url);
240-
var request = new HttpRequestMessage(HttpMethod.Delete, url)
241-
{
242-
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
243-
Version = HttpVersion.Version20,
244-
#endif
245-
};
230+
var request = new HttpRequestMessage(HttpMethod.Delete, url);
246231
var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
247232

248233
if (response.StatusCode == HttpStatusCode.NotFound)

src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/SendUtils.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,6 @@ public static async Task SendMessages(Uri sendUrl, IDuplexPipe application, Http
4141
// Send them in a single post
4242
var request = new HttpRequestMessage(HttpMethod.Post, sendUrl);
4343

44-
#if NETSTANDARD2_1_OR_GREATER || NET7_0_OR_GREATER
45-
// HttpClient gracefully falls back to HTTP/1.1, so it's fine to set the preferred version to a higher version
46-
request.Version = HttpVersion.Version20;
47-
#endif
48-
4944
request.Content = new ReadOnlySequenceContent(buffer);
5045

5146
// ResponseHeadersRead instructs SendAsync to return once headers are read

src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsTransport.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,7 @@ public async Task StartAsync(Uri url, TransferFormat transferFormat, Cancellatio
5555

5656
Log.StartTransport(_logger, transferFormat);
5757

58-
var request = new HttpRequestMessage(HttpMethod.Get, url)
59-
{
60-
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
61-
Version = HttpVersion.Version20,
62-
#endif
63-
};
58+
var request = new HttpRequestMessage(HttpMethod.Get, url);
6459
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
6560

6661
HttpResponseMessage? response = null;

src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ private async ValueTask<WebSocket> DefaultWebSocketFactory(WebSocketConnectionCo
9191
}
9292
}
9393

94+
#if NET7_0_OR_GREATER
95+
var allowHttp2 = true;
96+
#endif
97+
9498
if (!isBrowser)
9599
{
96100
if (context.Options.Cookies != null)
@@ -106,6 +110,11 @@ private async ValueTask<WebSocket> DefaultWebSocketFactory(WebSocketConnectionCo
106110
if (context.Options.Credentials != null)
107111
{
108112
webSocket.Options.Credentials = context.Options.Credentials;
113+
// Negotiate Auth isn't supported over HTTP/2 and HttpClient does not gracefully fallback to HTTP/1.1 in that case
114+
// https://github.com/dotnet/runtime/issues/1582
115+
#if NET7_0_OR_GREATER
116+
allowHttp2 = false;
117+
#endif
109118
}
110119

111120
var originalProxy = webSocket.Options.Proxy;
@@ -117,12 +126,20 @@ private async ValueTask<WebSocket> DefaultWebSocketFactory(WebSocketConnectionCo
117126
if (context.Options.UseDefaultCredentials != null)
118127
{
119128
webSocket.Options.UseDefaultCredentials = context.Options.UseDefaultCredentials.Value;
129+
if (context.Options.UseDefaultCredentials.Value)
130+
{
131+
// Negotiate Auth isn't supported over HTTP/2 and HttpClient does not gracefully fallback to HTTP/1.1 in that case
132+
// https://github.com/dotnet/runtime/issues/1582
133+
#if NET7_0_OR_GREATER
134+
allowHttp2 = false;
135+
#endif
136+
}
120137
}
121138

122139
context.Options.WebSocketConfiguration?.Invoke(webSocket.Options);
123140

124141
#if NET7_0_OR_GREATER
125-
if (webSocket.Options.HttpVersion >= HttpVersion.Version20)
142+
if (webSocket.Options.HttpVersion >= HttpVersion.Version20 && allowHttp2)
126143
{
127144
// Reset options we set on the users' behalf since they are already on the HttpClient that we're passing to ConnectAsync
128145
// And ConnectAsync will throw if these options are set on the ClientWebSocketOptions
@@ -148,6 +165,19 @@ private async ValueTask<WebSocket> DefaultWebSocketFactory(WebSocketConnectionCo
148165
}
149166
}
150167

168+
if (!allowHttp2 && webSocket.Options.HttpVersion >= HttpVersion.Version20)
169+
{
170+
// We shouldn't fallback to HTTP/1.1 if the user explicitly states
171+
if (webSocket.Options.HttpVersionPolicy == HttpVersionPolicy.RequestVersionOrLower)
172+
{
173+
webSocket.Options.HttpVersion = HttpVersion.Version11;
174+
}
175+
else
176+
{
177+
throw new InvalidOperationException("Negotiate Authentication doesn't work with HTTP/2 or higher.");
178+
}
179+
}
180+
151181
static bool IsX509CertificateCollectionEqual(X509CertificateCollection? left, X509CertificateCollection? right)
152182
{
153183
var leftCount = left?.Count ?? 0;

0 commit comments

Comments
 (0)