Skip to content

Commit 4f2214a

Browse files
authored
Close connection GoAway for IIS and HttpSys (#32096)
1 parent 0ecf4c1 commit 4f2214a

File tree

16 files changed

+224
-10
lines changed

16 files changed

+224
-10
lines changed

src/Http/Http.Abstractions/src/ConnectionInfo.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,13 @@ public abstract class ConnectionInfo
4848
/// </summary>
4949
/// <returns>Asynchronously returns an <see cref="X509Certificate2" />. Can be null.</returns>
5050
public abstract Task<X509Certificate2?> GetClientCertificateAsync(CancellationToken cancellationToken = new CancellationToken());
51+
52+
/// <summary>
53+
/// Close connection gracefully.
54+
/// </summary>
55+
public virtual void RequestClose()
56+
{
57+
58+
}
5159
}
5260
}

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ abstract Microsoft.AspNetCore.Http.HttpRequest.ContentType.get -> string?
2525
static Microsoft.AspNetCore.Builder.UseExtensions.Use(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, System.Func<Microsoft.AspNetCore.Http.HttpContext!, Microsoft.AspNetCore.Http.RequestDelegate!, System.Threading.Tasks.Task!>! middleware) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
2626
static Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.UseMiddleware(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, System.Type! middleware, params object?[]! args) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
2727
static Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.UseMiddleware<TMiddleware>(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, params object?[]! args) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
28+
virtual Microsoft.AspNetCore.Http.ConnectionInfo.RequestClose() -> void
2829
virtual Microsoft.AspNetCore.Http.WebSocketManager.AcceptWebSocketAsync(Microsoft.AspNetCore.Http.WebSocketAcceptContext! acceptContext) -> System.Threading.Tasks.Task<System.Net.WebSockets.WebSocket!>!
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
using Microsoft.AspNetCore.Connections.Features;
6+
7+
namespace Microsoft.AspNetCore.Http.Features
8+
{
9+
/// <summary>
10+
/// Default implementation of <see cref="IConnectionLifetimeNotificationFeature"/>.
11+
/// </summary>
12+
internal sealed class DefaultConnectionLifetimeNotificationFeature : IConnectionLifetimeNotificationFeature
13+
{
14+
private readonly IHttpResponseFeature? _httpResponseFeature;
15+
16+
/// <summary>
17+
///
18+
/// </summary>
19+
/// <param name="httpResponseFeature"></param>
20+
public DefaultConnectionLifetimeNotificationFeature(IHttpResponseFeature? httpResponseFeature)
21+
{
22+
_httpResponseFeature = httpResponseFeature;
23+
}
24+
25+
///<inheritdoc/>
26+
public CancellationToken ConnectionClosedRequested { get; set; }
27+
28+
///<inheritdoc/>
29+
public void RequestClose()
30+
{
31+
if (_httpResponseFeature != null)
32+
{
33+
if (!_httpResponseFeature.HasStarted)
34+
{
35+
_httpResponseFeature.Headers.Connection = "close";
36+
}
37+
}
38+
}
39+
}
40+
}

src/Http/Http/src/Internal/DefaultConnectionInfo.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Threading;
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.Http.Features;
10+
using Microsoft.AspNetCore.Connections.Features;
1011

1112
namespace Microsoft.AspNetCore.Http
1213
{
@@ -15,6 +16,7 @@ internal sealed class DefaultConnectionInfo : ConnectionInfo
1516
// Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624
1617
private readonly static Func<IFeatureCollection, IHttpConnectionFeature> _newHttpConnectionFeature = f => new HttpConnectionFeature();
1718
private readonly static Func<IFeatureCollection, ITlsConnectionFeature> _newTlsConnectionFeature = f => new TlsConnectionFeature();
19+
private readonly static Func<IFeatureCollection, IConnectionLifetimeNotificationFeature> _newConnectionLifetime = f => new DefaultConnectionLifetimeNotificationFeature(f.Get<IHttpResponseFeature>());
1820

1921
private FeatureReferences<FeatureInterfaces> _features;
2022

@@ -23,7 +25,7 @@ public DefaultConnectionInfo(IFeatureCollection features)
2325
Initialize(features);
2426
}
2527

26-
public void Initialize( IFeatureCollection features)
28+
public void Initialize(IFeatureCollection features)
2729
{
2830
_features.Initalize(features);
2931
}
@@ -44,6 +46,9 @@ public void Uninitialize()
4446
private ITlsConnectionFeature TlsConnectionFeature=>
4547
_features.Fetch(ref _features.Cache.TlsConnection, _newTlsConnectionFeature)!;
4648

49+
private IConnectionLifetimeNotificationFeature ConnectionLifetime =>
50+
_features.Fetch(ref _features.Cache.ConnectionLifetime, _newConnectionLifetime)!;
51+
4752
/// <inheritdoc />
4853
public override string Id
4954
{
@@ -86,10 +91,16 @@ public override X509Certificate2? ClientCertificate
8691
return TlsConnectionFeature.GetClientCertificateAsync(cancellationToken);
8792
}
8893

94+
public override void RequestClose()
95+
{
96+
ConnectionLifetime.RequestClose();
97+
}
98+
8999
struct FeatureInterfaces
90100
{
91101
public IHttpConnectionFeature? Connection;
92102
public ITlsConnectionFeature? TlsConnection;
103+
public IConnectionLifetimeNotificationFeature? ConnectionLifetime;
93104
}
94105
}
95106
}

src/Http/Http/src/Microsoft.AspNetCore.Http.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
</ItemGroup>
2424

2525
<ItemGroup>
26+
<Reference Include="Microsoft.AspNetCore.Connections.Abstractions" />
2627
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
2728
<Reference Include="Microsoft.AspNetCore.WebUtilities" />
2829
<Reference Include="Microsoft.Extensions.ObjectPool" />

src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ internal partial class RequestContext :
4040
IHttpSysRequestInfoFeature,
4141
IHttpResponseTrailersFeature,
4242
IHttpResetFeature,
43-
IHttpSysRequestDelegationFeature
43+
IHttpSysRequestDelegationFeature,
44+
IConnectionLifetimeNotificationFeature
4445
{
4546
private IFeatureCollection? _features;
4647
private bool _enableResponseCaching;
@@ -387,6 +388,11 @@ string IHttpConnectionFeature.ConnectionId
387388
return null;
388389
}
389390

391+
internal IConnectionLifetimeNotificationFeature? GetConnectionLifetimeNotificationFeature()
392+
{
393+
return this;
394+
}
395+
390396
/* TODO: https://github.com/aspnet/HttpSysServer/issues/231
391397
byte[] ITlsTokenBindingFeature.GetProvidedTokenBindingId() => Request.GetProvidedTokenBindingId();
392398
@@ -602,6 +608,8 @@ IHeaderDictionary IHttpResponseTrailersFeature.Trailers
602608

603609
public bool CanDelegate => Request.CanDelegate;
604610

611+
CancellationToken IConnectionLifetimeNotificationFeature.ConnectionClosedRequested { get; set; }
612+
605613
internal async Task OnResponseStart()
606614
{
607615
if (_responseStarted)
@@ -728,5 +736,14 @@ public void DelegateRequest(DelegationRule destination)
728736
Delegate(destination);
729737
_responseStarted = true;
730738
}
739+
740+
void IConnectionLifetimeNotificationFeature.RequestClose()
741+
{
742+
// Set the connection close feature if the response hasn't sent headers as yet
743+
if (!Response.HasStarted)
744+
{
745+
Response.Headers[HeaderNames.Connection] = "close";
746+
}
747+
}
731748
}
732749
}

src/Servers/HttpSys/src/StandardFeatureCollection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection
3131
{ typeof(IHttpSysRequestInfoFeature), _identityFunc },
3232
{ typeof(IHttpResponseTrailersFeature), ctx => ctx.GetResponseTrailersFeature() },
3333
{ typeof(IHttpResetFeature), ctx => ctx.GetResetFeature() },
34+
{ typeof(IConnectionLifetimeNotificationFeature), ctx => ctx.GetConnectionLifetimeNotificationFeature() },
3435
};
3536

3637
private readonly RequestContext _featureContext;

src/Servers/HttpSys/test/FunctionalTests/Http2Tests.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ await h2Connection.ReceiveHeadersAsync(3, endStream: true, decodedHeaders =>
359359

360360
[ConditionalFact]
361361
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_19H2, SkipReason = "GoAway support was added in Win10_19H2.")]
362-
public async Task ConnectionClose_OSSupport_SendsGoAway()
362+
public async Task ConnectionHeaderClose_OSSupport_SendsGoAway()
363363
{
364364
using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
365365
{
@@ -396,6 +396,45 @@ await h2Connection.ReceiveHeadersAsync(1, decodedHeaders =>
396396
.Build().RunAsync();
397397
}
398398

399+
[ConditionalFact]
400+
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_19H2, SkipReason = "GoAway support was added in Win10_19H2.")]
401+
public async Task ConnectionRequestClose_OSSupport_SendsGoAway()
402+
{
403+
using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
404+
{
405+
httpContext.Connection.RequestClose();
406+
return Task.FromResult(0);
407+
});
408+
409+
await new HostBuilder()
410+
.UseHttp2Cat(address, async h2Connection =>
411+
{
412+
await h2Connection.InitializeConnectionAsync();
413+
414+
h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1.");
415+
416+
await h2Connection.StartStreamAsync(1, Http2Utilities.BrowserRequestHeaders, endStream: true);
417+
418+
var goAwayFrame = await h2Connection.ReceiveFrameAsync();
419+
h2Connection.VerifyGoAway(goAwayFrame, int.MaxValue, Http2ErrorCode.NO_ERROR);
420+
421+
await h2Connection.ReceiveHeadersAsync(1, decodedHeaders =>
422+
{
423+
// HTTP/2 filters out the connection header
424+
Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection));
425+
Assert.Equal("200", decodedHeaders[HeaderNames.Status]);
426+
});
427+
428+
var dataFrame = await h2Connection.ReceiveFrameAsync();
429+
Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0);
430+
431+
// Http.Sys doesn't send a final GoAway unless we ignore the first one and send 200 additional streams.
432+
433+
h2Connection.Logger.LogInformation("Connection stopped.");
434+
})
435+
.Build().RunAsync();
436+
}
437+
399438
[ConditionalFact]
400439
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_19H2, SkipReason = "GoAway support was added in Win10_19H2.")]
401440
public async Task ConnectionClose_AdditionalRequests_ReceivesSecondGoAway()

src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,4 +601,14 @@ http_reset_stream(
601601
pHttpResponse->ResetStream(errorCode);
602602
}
603603

604+
EXTERN_C __MIDL_DECLSPEC_DLLEXPORT
605+
HRESULT
606+
http_response_set_need_goaway(
607+
_In_ IN_PROCESS_HANDLER* pInProcessHandler
608+
)
609+
{
610+
IHttpResponse4* pHttpResponse = (IHttpResponse4*)pInProcessHandler->QueryHttpContext()->GetResponse();
611+
pHttpResponse->SetNeedGoAway();
612+
return 0;
613+
}
604614
// End of export

src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
using System.Security.Cryptography.X509Certificates;
1313
using System.Threading;
1414
using System.Threading.Tasks;
15+
using Microsoft.AspNetCore.Connections.Features;
1516
using Microsoft.AspNetCore.Http;
1617
using Microsoft.AspNetCore.Http.Features;
1718
using Microsoft.AspNetCore.Http.Features.Authentication;
18-
using Microsoft.AspNetCore.HttpSys.Internal;
1919
using Microsoft.AspNetCore.Server.IIS.Core.IO;
2020
using Microsoft.AspNetCore.WebUtilities;
2121
using Microsoft.Extensions.Logging;
@@ -35,7 +35,8 @@ internal partial class IISHttpContext : IFeatureCollection,
3535
IHttpBodyControlFeature,
3636
IHttpMaxRequestBodySizeFeature,
3737
IHttpResponseTrailersFeature,
38-
IHttpResetFeature
38+
IHttpResetFeature,
39+
IConnectionLifetimeNotificationFeature
3940
{
4041
private int _featureRevision;
4142
private string? _httpProtocolVersion;
@@ -444,7 +445,7 @@ unsafe X509Certificate2? ITlsConnectionFeature.ClientCertificate
444445
internal IHttpResponseTrailersFeature? GetResponseTrailersFeature()
445446
{
446447
// Check version is above 2.
447-
if (HttpVersion >= System.Net.HttpVersion.Version20 && NativeMethods.HttpSupportTrailer(_requestNativeHandle))
448+
if (HttpVersion >= System.Net.HttpVersion.Version20 && NativeMethods.HttpHasResponse4(_requestNativeHandle))
448449
{
449450
return this;
450451
}
@@ -458,10 +459,12 @@ IHeaderDictionary IHttpResponseTrailersFeature.Trailers
458459
set => ResponseTrailers = value;
459460
}
460461

462+
CancellationToken IConnectionLifetimeNotificationFeature.ConnectionClosedRequested { get; set; }
463+
461464
internal IHttpResetFeature? GetResetFeature()
462465
{
463466
// Check version is above 2.
464-
if (HttpVersion >= System.Net.HttpVersion.Version20 && NativeMethods.HttpSupportTrailer(_requestNativeHandle))
467+
if (HttpVersion >= System.Net.HttpVersion.Version20 && NativeMethods.HttpHasResponse4(_requestNativeHandle))
465468
{
466469
return this;
467470
}
@@ -496,5 +499,15 @@ private void DisableCompression()
496499
var serverVariableFeature = (IServerVariablesFeature)this;
497500
serverVariableFeature["IIS_EnableDynamicCompression"] = "0";
498501
}
502+
503+
void IConnectionLifetimeNotificationFeature.RequestClose()
504+
{
505+
// Set the connection close feature if the response hasn't sent headers as yet
506+
if (!HasResponseStarted)
507+
{
508+
ResponseHeaders.Connection = ConnectionClose;
509+
}
510+
511+
}
499512
}
500513
}

src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal partial class IISHttpContext
3131
private static readonly Type IHttpMaxRequestBodySizeFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
3232
private static readonly Type IHttpResponseTrailersFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResponseTrailersFeature);
3333
private static readonly Type IHttpResetFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResetFeature);
34+
private static readonly Type IConnectionLifetimeNotificationFeature = typeof(global::Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeNotificationFeature);
3435
private static readonly Type IHttpActivityFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpActivityFeature);
3536

3637
private object? _currentIHttpRequestFeature;
@@ -55,6 +56,7 @@ internal partial class IISHttpContext
5556
private object? _currentIHttpMaxRequestBodySizeFeature;
5657
private object? _currentIHttpResponseTrailersFeature;
5758
private object? _currentIHttpResetFeature;
59+
private object? _currentIConnectionLifetimeNotificationFeature;
5860
private object? _currentIHttpActivityFeature;
5961

6062
private void Initialize()
@@ -74,6 +76,7 @@ private void Initialize()
7476
_currentITlsConnectionFeature = this;
7577
_currentIHttpResponseTrailersFeature = GetResponseTrailersFeature();
7678
_currentIHttpResetFeature = GetResetFeature();
79+
_currentIConnectionLifetimeNotificationFeature = this;
7780

7881
_currentIHttpActivityFeature = null;
7982
}
@@ -172,6 +175,10 @@ private void Initialize()
172175
{
173176
return _currentIHttpResetFeature;
174177
}
178+
if (key == IConnectionLifetimeNotificationFeature)
179+
{
180+
return _currentIConnectionLifetimeNotificationFeature;
181+
}
175182
if (key == IHttpActivityFeature)
176183
{
177184
return _currentIHttpActivityFeature;
@@ -282,14 +289,17 @@ internal void FastFeatureSet(Type key, object? feature)
282289
if (key == IHttpMaxRequestBodySizeFeature)
283290
{
284291
_currentIHttpMaxRequestBodySizeFeature = feature;
292+
return;
285293
}
286294
if (key == IHttpResponseTrailersFeature)
287295
{
288296
_currentIHttpResponseTrailersFeature = feature;
297+
return;
289298
}
290299
if (key == IHttpResetFeature)
291300
{
292301
_currentIHttpResetFeature = feature;
302+
return;
293303
}
294304
if (key == IHttpActivityFeature)
295305
{
@@ -298,7 +308,12 @@ internal void FastFeatureSet(Type key, object? feature)
298308
if (key == IISHttpContextType)
299309
{
300310
throw new InvalidOperationException("Cannot set IISHttpContext in feature collection");
301-
};
311+
}
312+
if (key == IConnectionLifetimeNotificationFeature)
313+
{
314+
_currentIConnectionLifetimeNotificationFeature = feature;
315+
return;
316+
}
302317
ExtraFeatureSet(key, feature);
303318
}
304319

src/Servers/IIS/IIS/src/Core/IISHttpContext.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ internal abstract partial class IISHttpContext : NativeRequestContext, IThreadPo
7272
private const string NtlmString = "NTLM";
7373
private const string NegotiateString = "Negotiate";
7474
private const string BasicString = "Basic";
75+
private const string ConnectionClose = "close";
7576

7677
internal unsafe IISHttpContext(
7778
MemoryPool<byte> memoryPool,
@@ -406,6 +407,15 @@ public unsafe void SetResponseHeaders()
406407
// This copies data into the underlying buffer
407408
NativeMethods.HttpSetResponseStatusCode(_requestNativeHandle, (ushort)StatusCode, reasonPhrase);
408409

410+
if (HttpVersion >= System.Net.HttpVersion.Version20 && NativeMethods.HttpHasResponse4(_requestNativeHandle))
411+
{
412+
// Check if connection close is set, if so setting goaway
413+
if (string.Equals(ConnectionClose, HttpResponseHeaders[HeaderNames.Connection], StringComparison.OrdinalIgnoreCase))
414+
{
415+
NativeMethods.HttpSetNeedGoAway(_requestNativeHandle);
416+
}
417+
}
418+
409419
HttpResponseHeaders.IsReadOnly = true;
410420
foreach (var headerPair in HttpResponseHeaders)
411421
{

0 commit comments

Comments
 (0)