Fix WebSockets Negotiate Auth in Kestrel (#26480)
* Don't close connections after upgrade requests without a 101 response * Add test * Add DefautCredentials_WebSocket_Success
This commit is contained in:
parent
8eb9603a9c
commit
96c082f285
|
|
@ -9,6 +9,7 @@
|
||||||
<Reference Include="Microsoft.AspNetCore.Authentication.Negotiate" />
|
<Reference Include="Microsoft.AspNetCore.Authentication.Negotiate" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Routing" />
|
<Reference Include="Microsoft.AspNetCore.Routing" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.WebSockets" />
|
||||||
<Reference Include="Microsoft.Extensions.Hosting" />
|
<Reference Include="Microsoft.Extensions.Hosting" />
|
||||||
<Reference Include="System.Net.Http.WinHttpHandler" />
|
<Reference Include="System.Net.Http.WinHttpHandler" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
|
@ -23,7 +26,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
{
|
{
|
||||||
// In theory this would work on Linux and Mac, but the client would require explicit credentials.
|
// In theory this would work on Linux and Mac, but the client would require explicit credentials.
|
||||||
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
|
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
|
||||||
public class NegotiateHandlerFunctionalTests
|
public class NegotiateHandlerFunctionalTests : LoggedTest
|
||||||
{
|
{
|
||||||
private static readonly Version Http11Version = new Version(1, 1);
|
private static readonly Version Http11Version = new Version(1, 1);
|
||||||
private static readonly Version Http2Version = new Version(2, 0);
|
private static readonly Version Http2Version = new Version(2, 0);
|
||||||
|
|
@ -109,6 +112,34 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
Assert.Equal(Http11Version, result.Version); // HTTP/2 downgrades.
|
Assert.Equal(Http11Version, result.Version); // HTTP/2 downgrades.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ConditionalFact]
|
||||||
|
public async Task DefautCredentials_WebSocket_Success()
|
||||||
|
{
|
||||||
|
using var host = await CreateHostAsync();
|
||||||
|
|
||||||
|
var address = host.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>().Addresses.First().Replace("https://", "wss://");
|
||||||
|
|
||||||
|
using var webSocket = new ClientWebSocket
|
||||||
|
{
|
||||||
|
Options =
|
||||||
|
{
|
||||||
|
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
|
||||||
|
UseDefaultCredentials = true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
|
await webSocket.ConnectAsync(new Uri($"{address}/AuthenticateWebSocket"), cts.Token);
|
||||||
|
|
||||||
|
var receiveBuffer = new byte[13];
|
||||||
|
var receiveResult = await webSocket.ReceiveAsync(receiveBuffer, cts.Token);
|
||||||
|
|
||||||
|
Assert.True(receiveResult.EndOfMessage);
|
||||||
|
Assert.Equal(WebSocketMessageType.Text, receiveResult.MessageType);
|
||||||
|
Assert.Equal("Hello World!", Encoding.UTF8.GetString(receiveBuffer, 0, receiveResult.Count));
|
||||||
|
}
|
||||||
|
|
||||||
public static IEnumerable<object[]> HttpOrders =>
|
public static IEnumerable<object[]> HttpOrders =>
|
||||||
new List<object[]>
|
new List<object[]>
|
||||||
{
|
{
|
||||||
|
|
@ -232,9 +263,10 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
Assert.Equal(Http11Version, result.Version); // HTTP/2 downgrades.
|
Assert.Equal(Http11Version, result.Version); // HTTP/2 downgrades.
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<IHost> CreateHostAsync(Action<NegotiateOptions> configureOptions = null)
|
private Task<IHost> CreateHostAsync(Action<NegotiateOptions> configureOptions = null)
|
||||||
{
|
{
|
||||||
var builder = new HostBuilder()
|
var builder = new HostBuilder()
|
||||||
|
.ConfigureServices(AddTestLogging)
|
||||||
.ConfigureServices(services => services
|
.ConfigureServices(services => services
|
||||||
.AddRouting()
|
.AddRouting()
|
||||||
.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
|
.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
|
||||||
|
|
@ -252,6 +284,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
{
|
{
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
app.UseWebSockets();
|
||||||
app.UseEndpoints(ConfigureEndpoints);
|
app.UseEndpoints(ConfigureEndpoints);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -289,6 +322,27 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate
|
||||||
await context.Response.WriteAsync(name);
|
await context.Response.WriteAsync(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Map("/AuthenticateWebSocket", async context =>
|
||||||
|
{
|
||||||
|
if (!context.User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
await context.ChallengeAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 400;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.False(string.IsNullOrEmpty(context.User.Identity.Name), "name");
|
||||||
|
|
||||||
|
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
|
||||||
|
await webSocket.SendAsync(Encoding.UTF8.GetBytes("Hello World!"), WebSocketMessageType.Text, endOfMessage: true, context.RequestAborted);
|
||||||
|
});
|
||||||
|
|
||||||
builder.Map("/AlreadyAuthenticated", async context =>
|
builder.Map("/AlreadyAuthenticated", async context =>
|
||||||
{
|
{
|
||||||
Assert.Equal("HTTP/1.1", context.Request.Protocol); // Not HTTP/2
|
Assert.Equal("HTTP/1.1", context.Request.Protocol); // Not HTTP/2
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
private readonly Pipe _requestBodyPipe;
|
private readonly Pipe _requestBodyPipe;
|
||||||
private ReadResult _readResult;
|
private ReadResult _readResult;
|
||||||
|
|
||||||
public Http1ChunkedEncodingMessageBody(bool keepAlive, Http1Connection context)
|
public Http1ChunkedEncodingMessageBody(Http1Connection context, bool keepAlive)
|
||||||
: base(context)
|
: base(context, keepAlive)
|
||||||
{
|
{
|
||||||
RequestKeepAlive = keepAlive;
|
|
||||||
_requestBodyPipe = CreateRequestBodyPipe(context);
|
_requestBodyPipe = CreateRequestBodyPipe(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ using Microsoft.AspNetCore.Connections;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException;
|
|
||||||
|
|
||||||
internal sealed class Http1ContentLengthMessageBody : Http1MessageBody
|
internal sealed class Http1ContentLengthMessageBody : Http1MessageBody
|
||||||
{
|
{
|
||||||
private ReadResult _readResult;
|
private ReadResult _readResult;
|
||||||
|
|
@ -23,12 +21,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
private bool _finalAdvanceCalled;
|
private bool _finalAdvanceCalled;
|
||||||
private bool _cannotResetInputPipe;
|
private bool _cannotResetInputPipe;
|
||||||
|
|
||||||
public Http1ContentLengthMessageBody(bool keepAlive, long contentLength, Http1Connection context)
|
public Http1ContentLengthMessageBody(Http1Connection context, long contentLength, bool keepAlive)
|
||||||
: base(context)
|
: base(context, keepAlive)
|
||||||
{
|
{
|
||||||
RequestKeepAlive = keepAlive;
|
|
||||||
_contentLength = contentLength;
|
_contentLength = contentLength;
|
||||||
_unexaminedInputLength = _contentLength;
|
_unexaminedInputLength = contentLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default)
|
public override ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default)
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
protected readonly Http1Connection _context;
|
protected readonly Http1Connection _context;
|
||||||
protected bool _completed;
|
protected bool _completed;
|
||||||
|
|
||||||
protected Http1MessageBody(Http1Connection context) : base(context)
|
protected Http1MessageBody(Http1Connection context, bool keepAlive) : base(context)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
RequestKeepAlive = keepAlive;
|
||||||
}
|
}
|
||||||
|
|
||||||
[StackTraceHidden]
|
[StackTraceHidden]
|
||||||
|
|
@ -118,14 +119,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
// see also http://tools.ietf.org/html/rfc2616#section-4.4
|
// see also http://tools.ietf.org/html/rfc2616#section-4.4
|
||||||
var keepAlive = httpVersion != HttpVersion.Http10;
|
var keepAlive = httpVersion != HttpVersion.Http10;
|
||||||
|
|
||||||
var upgrade = false;
|
var upgrade = false;
|
||||||
|
|
||||||
if (headers.HasConnection)
|
if (headers.HasConnection)
|
||||||
{
|
{
|
||||||
var connectionOptions = HttpHeaders.ParseConnection(headers.HeaderConnection);
|
var connectionOptions = HttpHeaders.ParseConnection(headers.HeaderConnection);
|
||||||
|
|
||||||
upgrade = (connectionOptions & ConnectionOptions.Upgrade) != 0;
|
upgrade = (connectionOptions & ConnectionOptions.Upgrade) != 0;
|
||||||
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) != 0;
|
keepAlive = keepAlive || (connectionOptions & ConnectionOptions.KeepAlive) != 0;
|
||||||
|
keepAlive = keepAlive && (connectionOptions & ConnectionOptions.Close) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (upgrade)
|
if (upgrade)
|
||||||
|
|
@ -136,7 +138,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
}
|
}
|
||||||
|
|
||||||
context.OnTrailersComplete(); // No trailers for these.
|
context.OnTrailersComplete(); // No trailers for these.
|
||||||
return new Http1UpgradeMessageBody(context);
|
return new Http1UpgradeMessageBody(context, keepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (headers.HasTransferEncoding)
|
if (headers.HasTransferEncoding)
|
||||||
|
|
@ -157,7 +159,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
// TODO may push more into the wrapper rather than just calling into the message body
|
// TODO may push more into the wrapper rather than just calling into the message body
|
||||||
// NBD for now.
|
// NBD for now.
|
||||||
return new Http1ChunkedEncodingMessageBody(keepAlive, context);
|
return new Http1ChunkedEncodingMessageBody(context, keepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (headers.ContentLength.HasValue)
|
if (headers.ContentLength.HasValue)
|
||||||
|
|
@ -169,7 +171,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
return keepAlive ? MessageBody.ZeroContentLengthKeepAlive : MessageBody.ZeroContentLengthClose;
|
return keepAlive ? MessageBody.ZeroContentLengthKeepAlive : MessageBody.ZeroContentLengthClose;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Http1ContentLengthMessageBody(keepAlive, contentLength, context);
|
return new Http1ContentLengthMessageBody(context, contentLength, keepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we got here, request contains no Content-Length or Transfer-Encoding header.
|
// If we got here, request contains no Content-Length or Transfer-Encoding header.
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class Http1UpgradeMessageBody : Http1MessageBody
|
internal sealed class Http1UpgradeMessageBody : Http1MessageBody
|
||||||
{
|
{
|
||||||
public Http1UpgradeMessageBody(Http1Connection context)
|
public Http1UpgradeMessageBody(Http1Connection context, bool keepAlive)
|
||||||
: base(context)
|
: base(context, keepAlive)
|
||||||
{
|
{
|
||||||
RequestUpgrade = true;
|
RequestUpgrade = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1113,13 +1113,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
RejectNonBodyTransferEncodingResponse(appCompleted);
|
RejectNonBodyTransferEncodingResponse(appCompleted);
|
||||||
}
|
}
|
||||||
|
else if (StatusCode == StatusCodes.Status101SwitchingProtocols)
|
||||||
|
{
|
||||||
|
_keepAlive = false;
|
||||||
|
}
|
||||||
else if (!hasTransferEncoding && !responseHeaders.ContentLength.HasValue)
|
else if (!hasTransferEncoding && !responseHeaders.ContentLength.HasValue)
|
||||||
{
|
{
|
||||||
if (StatusCode == StatusCodes.Status101SwitchingProtocols)
|
if ((appCompleted || !_canWriteResponseBody) && !_hasAdvanced) // Avoid setting contentLength of 0 if we wrote data before calling CreateResponseHeaders
|
||||||
{
|
|
||||||
_keepAlive = false;
|
|
||||||
}
|
|
||||||
else if ((appCompleted || !_canWriteResponseBody) && !_hasAdvanced) // Avoid setting contentLength of 0 if we wrote data before calling CreateResponseHeaders
|
|
||||||
{
|
{
|
||||||
// Don't set the Content-Length header automatically for HEAD requests, 204 responses, or 304 responses.
|
// Don't set the Content-Length header automatically for HEAD requests, 204 responses, or 304 responses.
|
||||||
if (CanAutoSetContentLengthZeroResponseHeader())
|
if (CanAutoSetContentLengthZeroResponseHeader())
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
|
||||||
|
|
@ -112,7 +113,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||||
});
|
});
|
||||||
|
|
||||||
http1Connection.Reset();
|
http1Connection.Reset();
|
||||||
http1Connection.InitializeBodyControl(new Http1ContentLengthMessageBody(keepAlive: true, 100, http1Connection));
|
http1Connection.InitializeBodyControl(new Http1ContentLengthMessageBody(http1Connection, contentLength: 100, keepAlive: true));
|
||||||
serviceContext.DateHeaderValueManager.OnHeartbeat(DateTimeOffset.UtcNow);
|
serviceContext.DateHeaderValueManager.OnHeartbeat(DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
return http1Connection;
|
return http1Connection;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
|
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Tests;
|
using Microsoft.AspNetCore.Server.Kestrel.Tests;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
|
|
@ -343,5 +342,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
await appCompletedTcs.Task.DefaultTimeout();
|
await appCompletedTcs.Task.DefaultTimeout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DoesNotCloseConnectionWithout101Response()
|
||||||
|
{
|
||||||
|
var requestCount = 0;
|
||||||
|
|
||||||
|
await using (var server = new TestServer(async context =>
|
||||||
|
{
|
||||||
|
if (requestCount++ > 0)
|
||||||
|
{
|
||||||
|
await context.Features.Get<IHttpUpgradeFeature>().UpgradeAsync();
|
||||||
|
}
|
||||||
|
}, new TestServiceContext(LoggerFactory)))
|
||||||
|
{
|
||||||
|
using (var connection = server.CreateConnection())
|
||||||
|
{
|
||||||
|
await connection.SendEmptyGetWithUpgrade();
|
||||||
|
await connection.Receive(
|
||||||
|
"HTTP/1.1 200 OK",
|
||||||
|
$"Date: {server.Context.DateHeaderValue}",
|
||||||
|
"Content-Length: 0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
await connection.SendEmptyGetWithUpgrade();
|
||||||
|
await connection.Receive("HTTP/1.1 101 Switching Protocols",
|
||||||
|
"Connection: Upgrade",
|
||||||
|
$"Date: {server.Context.DateHeaderValue}",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue