Skip to content

Commit a382a0f

Browse files
authored
Make H2 WebSockets ignore MaxRequestBodySize and MinRequestBodyDataRate #42101 (#42269)
1 parent 1282215 commit a382a0f

File tree

10 files changed

+242
-117
lines changed

10 files changed

+242
-117
lines changed

src/Middleware/WebSockets/samples/EchoApp/EchoApp.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9+
<Reference Include="Microsoft.AspNetCore" />
910
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
1011
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
1112
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
@@ -14,4 +15,10 @@
1415
<Reference Include="Microsoft.Extensions.Logging.Console" />
1516
</ItemGroup>
1617

18+
<ItemGroup>
19+
<Content Update="appsettings.json">
20+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
21+
</Content>
22+
</ItemGroup>
23+
1724
</Project>
Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,89 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Net.WebSockets;
5+
using System.Text;
6+
using Microsoft.AspNetCore.Http.Features;
7+
48
namespace EchoApp;
59

610
public class Program
711
{
812
public static Task Main(string[] args)
913
{
10-
var host = new HostBuilder()
11-
.ConfigureWebHost(webHostBuilder =>
14+
var builder = WebApplication.CreateBuilder();
15+
var app = builder.Build();
16+
17+
app.UseDeveloperExceptionPage();
18+
app.UseWebSockets();
19+
20+
app.Use(async (context, next) =>
21+
{
22+
if (context.WebSockets.IsWebSocketRequest)
23+
{
24+
var webSocket = await context.WebSockets.AcceptWebSocketAsync(new WebSocketAcceptContext() { DangerousEnableCompression = true });
25+
await Echo(context, webSocket, app.Logger);
26+
return;
27+
}
28+
29+
await next(context);
30+
});
31+
32+
app.UseFileServer();
33+
34+
return app.RunAsync();
35+
}
36+
37+
private static async Task Echo(HttpContext context, WebSocket webSocket, ILogger logger)
38+
{
39+
var buffer = new byte[1024 * 4];
40+
var result = await webSocket.ReceiveAsync(buffer.AsMemory(), CancellationToken.None);
41+
LogFrame(logger, webSocket, result, buffer);
42+
while (result.MessageType != WebSocketMessageType.Close)
43+
{
44+
// If the client send "ServerClose", then they want a server-originated close to occur
45+
string content = "<<binary>>";
46+
if (result.MessageType == WebSocketMessageType.Text)
47+
{
48+
content = Encoding.UTF8.GetString(buffer, 0, result.Count);
49+
if (content.Equals("ServerClose"))
50+
{
51+
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing from Server", CancellationToken.None);
52+
logger.LogDebug($"Sent Frame Close: {WebSocketCloseStatus.NormalClosure} Closing from Server");
53+
return;
54+
}
55+
else if (content.Equals("ServerAbort"))
56+
{
57+
context.Abort();
58+
}
59+
}
60+
61+
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
62+
logger.LogDebug($"Sent Frame {result.MessageType}: Len={result.Count}, Fin={result.EndOfMessage}: {content}");
63+
64+
result = await webSocket.ReceiveAsync(buffer.AsMemory(), CancellationToken.None);
65+
LogFrame(logger, webSocket, result, buffer);
66+
}
67+
await webSocket.CloseAsync(webSocket.CloseStatus.Value, webSocket.CloseStatusDescription, CancellationToken.None);
68+
}
69+
70+
private static void LogFrame(ILogger logger, WebSocket webSocket, ValueWebSocketReceiveResult frame, byte[] buffer)
71+
{
72+
var close = frame.MessageType == WebSocketMessageType.Close;
73+
string message;
74+
if (close)
75+
{
76+
message = $"Close: {webSocket.CloseStatus.Value} {webSocket.CloseStatusDescription}";
77+
}
78+
else
79+
{
80+
string content = "<<binary>>";
81+
if (frame.MessageType == WebSocketMessageType.Text)
1282
{
13-
webHostBuilder
14-
.UseKestrel()
15-
.UseContentRoot(Directory.GetCurrentDirectory())
16-
.UseIISIntegration()
17-
.UseStartup<Startup>();
18-
})
19-
.Build();
20-
21-
return host.RunAsync();
83+
content = Encoding.UTF8.GetString(buffer, 0, frame.Count);
84+
}
85+
message = $"{frame.MessageType}: Len={frame.Count}, Fin={frame.EndOfMessage}: {content}";
86+
}
87+
logger.LogDebug("Received Frame " + message);
2288
}
2389
}

src/Middleware/WebSockets/samples/EchoApp/Startup.cs

Lines changed: 0 additions & 96 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Debug",
5+
"Microsoft.AspNetCore": "Trace"
6+
}
7+
}
8+
}

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ CancellationToken IHttpRequestLifetimeFeature.RequestAborted
149149

150150
bool IHttpUpgradeFeature.IsUpgradableRequest => IsUpgradableRequest;
151151

152-
bool IHttpExtendedConnectFeature.IsExtendedConnect => IsConnectRequest;
152+
bool IHttpExtendedConnectFeature.IsExtendedConnect => IsExtendedConnectRequest;
153153

154154
string? IHttpExtendedConnectFeature.Protocol => ConnectProtocol;
155155

@@ -195,7 +195,7 @@ bool IHttpBodyControlFeature.AllowSynchronousIO
195195
set => AllowSynchronousIO = value;
196196
}
197197

198-
bool IHttpMaxRequestBodySizeFeature.IsReadOnly => HasStartedConsumingRequestBody || IsUpgraded;
198+
bool IHttpMaxRequestBodySizeFeature.IsReadOnly => HasStartedConsumingRequestBody || IsUpgraded || IsExtendedConnectRequest;
199199

200200
long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize
201201
{
@@ -290,12 +290,12 @@ async Task<Stream> IHttpUpgradeFeature.UpgradeAsync()
290290

291291
async ValueTask<Stream> IHttpExtendedConnectFeature.AcceptAsync()
292292
{
293-
if (!IsConnectRequest)
293+
if (!IsExtendedConnectRequest)
294294
{
295295
throw new InvalidOperationException(CoreStrings.CannotAcceptNonConnectRequest);
296296
}
297297

298-
if (IsUpgraded)
298+
if (IsExtendedConnectAccepted)
299299
{
300300
throw new InvalidOperationException(CoreStrings.AcceptCannotBeCalledMultipleTimes);
301301
}
@@ -305,7 +305,7 @@ async ValueTask<Stream> IHttpExtendedConnectFeature.AcceptAsync()
305305
throw new InvalidOperationException(CoreStrings.ConnectStatusMustBe200);
306306
}
307307

308-
IsUpgraded = true;
308+
IsExtendedConnectAccepted = true;
309309

310310
await FlushAsync();
311311

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ public string TraceIdentifier
127127

128128
public bool IsUpgradableRequest { get; private set; }
129129
public bool IsUpgraded { get; set; }
130-
public bool IsConnectRequest { get; set; }
130+
public bool IsExtendedConnectRequest { get; set; }
131+
public bool IsExtendedConnectAccepted { get; set; }
131132
public IPAddress? RemoteIpAddress { get; set; }
132133
public int RemotePort { get; set; }
133134
public IPAddress? LocalIpAddress { get; set; }
@@ -359,6 +360,8 @@ public void Reset()
359360
_statusCode = StatusCodes.Status200OK;
360361
_reasonPhrase = null;
361362
IsUpgraded = false;
363+
IsExtendedConnectRequest = false;
364+
IsExtendedConnectAccepted = false;
362365

363366
var remoteEndPoint = RemoteEndPoint;
364367
RemoteIpAddress = remoteEndPoint?.Address;

src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ protected MessageBody(HttpProtocol context)
3737

3838
public bool RequestUpgrade { get; protected set; }
3939

40+
public bool ExtendedConnect { get; protected set; }
41+
4042
public virtual bool IsEmpty => false;
4143

4244
protected KestrelTrace Log => _context.ServiceContext.Log;
@@ -122,7 +124,7 @@ protected Task TryStartAsync()
122124
OnReadStarting();
123125
_context.HasStartedConsumingRequestBody = true;
124126

125-
if (!RequestUpgrade)
127+
if (!RequestUpgrade && !ExtendedConnect)
126128
{
127129
// Accessing TraceIdentifier will lazy-allocate a string ID.
128130
// Don't access TraceIdentifer unless logging is enabled.
@@ -150,7 +152,7 @@ protected void TryStop()
150152

151153
_stopped = true;
152154

153-
if (!RequestUpgrade)
155+
if (!RequestUpgrade && !ExtendedConnect)
154156
{
155157
// Accessing TraceIdentifier will lazy-allocate a string ID
156158
// Don't access TraceIdentifer unless logging is enabled.

src/Servers/Kestrel/Core/src/Internal/Http2/Http2MessageBody.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public Http2MessageBody(Http2Stream context)
1919
: base(context)
2020
{
2121
_context = context;
22+
ExtendedConnect = _context.IsExtendedConnectRequest;
2223
}
2324

2425
protected override void OnReadStarting()
@@ -51,6 +52,7 @@ public override void Reset()
5152
{
5253
base.Reset();
5354
_readResult = default;
55+
ExtendedConnect = _context.IsExtendedConnectRequest;
5456
}
5557

5658
public override void AdvanceTo(SequencePosition consumed, SequencePosition examined)
@@ -62,7 +64,12 @@ public override void AdvanceTo(SequencePosition consumed, SequencePosition exami
6264

6365
// The HTTP/2 flow control window cannot be larger than 2^31-1 which limits bytesRead.
6466
_context.OnDataRead((int)newlyExaminedBytes);
65-
AddAndCheckObservedBytes(newlyExaminedBytes);
67+
68+
// Don't limit extended CONNECT requests to the MaxRequestBodySize.
69+
if (!ExtendedConnect)
70+
{
71+
AddAndCheckObservedBytes(newlyExaminedBytes);
72+
}
6673
}
6774

6875
public override bool TryRead(out ReadResult readResult)

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ private bool TryValidatePseudoHeaders()
239239
return false;
240240
}
241241
ConnectProtocol = HttpRequestHeaders.HeaderProtocol;
242-
IsConnectRequest = true;
242+
IsExtendedConnectRequest = true;
243243
}
244244
// CONNECT - :scheme and :path must be excluded
245245
else if (!StringValues.IsNullOrEmpty(HttpRequestHeaders.HeaderScheme) || !StringValues.IsNullOrEmpty(HttpRequestHeaders.HeaderPath))

0 commit comments

Comments
 (0)