Skip to content

Commit 292df8e

Browse files
authored
ClientWebSocket Loopback Server tests (#117255)
* Extract helper from echo server * Refactor external server tests * WIP * Fix loopback echo server, add more tests * Add thread-safety option to Http2LoopbackServer * Test fixes, add more tests * Refactor LoopbackWebSocketServer, fix subprotocol tests * wip * Fixes * Refactoring * Fix H2 loopback * Add missing read-write tests, remove debug logging * Fixed and unified all tests with exception of send-receive * Fix external send-receive * Fixed most of the loopback send-receive * Removed WebSocketData * NetCoreServer fix, removed unused stuff * Fix browser build * remove ConfigureHttp2Options * Address PR feedback
1 parent b306971 commit 292df8e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2787
-1593
lines changed

src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,13 @@ public static partial class WebSockets
2222
public static readonly Uri RemoteEchoHeadersServer = new Uri("ws://" + Host + "/" + EchoHeadersHandler);
2323
public static readonly Uri SecureRemoteEchoHeadersServer = new Uri("wss://" + SecureHost + "/" + EchoHeadersHandler);
2424

25-
public static object[][] GetEchoServers()
26-
{
27-
if (PlatformDetection.IsFirefox)
28-
{
29-
// https://github.com/dotnet/runtime/issues/101115
30-
return new object[][] {
31-
new object[] { RemoteEchoServer },
32-
};
33-
}
34-
return new object[][] {
35-
new object[] { RemoteEchoServer },
36-
new object[] { SecureRemoteEchoServer },
37-
};
38-
}
39-
40-
public static object[][] GetEchoHeadersServers()
41-
{
42-
if (PlatformDetection.IsFirefox)
43-
{
44-
// https://github.com/dotnet/runtime/issues/101115
45-
return new object[][] {
46-
new object[] { RemoteEchoHeadersServer },
47-
};
48-
}
49-
return new object[][] {
50-
new object[] { RemoteEchoHeadersServer },
51-
new object[] { SecureRemoteEchoHeadersServer },
52-
};
53-
}
25+
public static Uri[] GetEchoServers() => PlatformDetection.IsFirefox
26+
? [ RemoteEchoServer ] // https://github.com/dotnet/runtime/issues/101115
27+
: [ RemoteEchoServer, SecureRemoteEchoServer ];
28+
29+
public static Uri[] GetEchoHeadersServers() => PlatformDetection.IsFirefox
30+
? [ RemoteEchoHeadersServer ] // https://github.com/dotnet/runtime/issues/101115
31+
: [ RemoteEchoHeadersServer, SecureRemoteEchoHeadersServer ];
5432
}
5533
}
5634
}

src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,68 @@ public class Http2LoopbackConnection : GenericLoopbackConnection
2828
private readonly TimeSpan _timeout;
2929
private int _lastStreamId;
3030
private bool _expectClientDisconnect;
31+
private readonly SemaphoreSlim? _readLock;
32+
private readonly SemaphoreSlim? _writeLock;
3133

3234
private readonly byte[] _prefix = new byte[24];
3335
public string PrefixString => Encoding.UTF8.GetString(_prefix, 0, _prefix.Length);
3436
public bool IsInvalid => _connectionSocket == null;
3537
public Stream Stream => _connectionStream;
3638
public Task<bool> SettingAckWaiter => _ignoredSettingsAckPromise?.Task;
3739

38-
private Http2LoopbackConnection(SocketWrapper socket, Stream stream, TimeSpan timeout, bool transparentPingResponse)
40+
private Http2LoopbackConnection(SocketWrapper socket, Stream stream, TimeSpan timeout, Http2Options httpOptions)
3941
{
4042
_connectionSocket = socket;
4143
_connectionStream = stream;
4244
_timeout = timeout;
43-
_transparentPingResponse = transparentPingResponse;
45+
_transparentPingResponse = httpOptions.EnableTransparentPingResponse;
46+
47+
if (httpOptions.EnsureThreadSafeIO)
48+
{
49+
_readLock = new SemaphoreSlim(1, 1);
50+
_writeLock = new SemaphoreSlim(1, 1);
51+
_connectionStream = CreateConcurrentConnectionStream(stream, _readLock, _writeLock);
52+
}
53+
54+
static Stream CreateConcurrentConnectionStream(Stream stream, SemaphoreSlim readLock, SemaphoreSlim writeLock)
55+
{
56+
return new DelegateStream(
57+
canReadFunc: () => true,
58+
canWriteFunc: () => true,
59+
readAsyncFunc: async (buffer, offset, count, cancellationToken) =>
60+
{
61+
await readLock.WaitAsync(cancellationToken);
62+
try
63+
{
64+
return await stream.ReadAsync(buffer, offset, count, cancellationToken);
65+
}
66+
finally
67+
{
68+
readLock.Release();
69+
}
70+
},
71+
writeAsyncFunc: async (buffer, offset, count, cancellationToken) =>
72+
{
73+
await writeLock.WaitAsync(cancellationToken);
74+
try
75+
{
76+
await stream.WriteAsync(buffer, offset, count, cancellationToken);
77+
await stream.FlushAsync(cancellationToken);
78+
}
79+
finally
80+
{
81+
writeLock.Release();
82+
}
83+
},
84+
disposeFunc: (disposing) =>
85+
{
86+
if (disposing)
87+
{
88+
stream.Dispose();
89+
}
90+
}
91+
);
92+
}
4493
}
4594

4695
public override string ToString()
@@ -83,7 +132,7 @@ public static async Task<Http2LoopbackConnection> CreateAsync(SocketWrapper sock
83132
stream = sslStream;
84133
}
85134

86-
var con = new Http2LoopbackConnection(socket, stream, timeout, httpOptions.EnableTransparentPingResponse);
135+
var con = new Http2LoopbackConnection(socket, stream, timeout, httpOptions);
87136
await con.ReadPrefixAsync().ConfigureAwait(false);
88137

89138
return con;

src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ public class Http2Options : GenericLoopbackOptions
185185
public bool ClientCertificateRequired { get; set; }
186186

187187
public bool EnableTransparentPingResponse { get; set; } = true;
188+
public bool EnsureThreadSafeIO { get; set; }
188189

189190
public Http2Options()
190191
{
@@ -216,7 +217,12 @@ public override async Task<GenericLoopbackConnection> CreateConnectionAsync(Sock
216217

217218
private static Http2Options CreateOptions(GenericLoopbackOptions options)
218219
{
219-
Http2Options http2Options = new Http2Options();
220+
if (options is Http2Options http2Options)
221+
{
222+
return http2Options;
223+
}
224+
225+
http2Options = new Http2Options();
220226
if (options != null)
221227
{
222228
http2Options.Address = options.Address;

src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,11 @@ public override async Task<GenericLoopbackConnection> CreateConnectionAsync(Sock
11451145

11461146
private static LoopbackServer.Options CreateOptions(GenericLoopbackOptions options)
11471147
{
1148+
if (options is LoopbackServer.Options { } loopbackOptions)
1149+
{
1150+
return loopbackOptions;
1151+
}
1152+
11481153
LoopbackServer.Options newOptions = new LoopbackServer.Options();
11491154
if (options != null)
11501155
{

src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Directory.Build.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<Project>
22
<PropertyGroup>
33
<RepositoryRoot Condition="'$(RepositoryRoot)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)../, global.json))/</RepositoryRoot>
4+
<CommonTestPath Condition="'$(CommonTestPath)' == ''">$([MSBuild]::NormalizeDirectory('$(RepositoryRoot)', 'src', 'libraries', 'Common', 'tests'))</CommonTestPath>
45
</PropertyGroup>
56

67
<Import Project="$([MSBuild]::NormalizePath($(RepositoryRoot), 'eng', 'testing', 'ForXHarness.Directory.Build.targets'))" />

src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs

Lines changed: 7 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -3,186 +3,33 @@
33

44
using System;
55
using System.Net.WebSockets;
6-
using System.Text;
7-
using System.Threading;
6+
using System.Net.Test.Common;
87
using System.Threading.Tasks;
98
using Microsoft.AspNetCore.Http;
109

1110
namespace NetCoreServer
1211
{
1312
public class EchoWebSocketHandler
1413
{
15-
private const int MaxBufferSize = 128 * 1024;
16-
1714
public static async Task InvokeAsync(HttpContext context)
1815
{
19-
QueryString queryString = context.Request.QueryString;
20-
bool replyWithPartialMessages = queryString.HasValue && queryString.Value.Contains("replyWithPartialMessages");
21-
bool replyWithEnhancedCloseMessage = queryString.HasValue && queryString.Value.Contains("replyWithEnhancedCloseMessage");
22-
23-
string subProtocol = context.Request.Query["subprotocol"];
24-
25-
if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay10sec"))
26-
{
27-
await Task.Delay(10000);
28-
}
29-
else if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay20sec"))
30-
{
31-
await Task.Delay(20000);
32-
}
33-
16+
var queryString = context.Request.QueryString.ToUriComponent(); // Returns empty string if request URI has no query
17+
WebSocketEchoOptions options = await WebSocketEchoHelper.ProcessOptions(queryString);
3418
try
3519
{
36-
if (!context.WebSockets.IsWebSocketRequest)
20+
WebSocket socket = await WebSocketAcceptHelper.AcceptAsync(context, options.SubProtocol);
21+
if (socket is null)
3722
{
38-
context.Response.StatusCode = 200;
39-
context.Response.ContentType = "text/plain";
40-
await context.Response.WriteAsync("Not a websocket request");
41-
4223
return;
4324
}
4425

45-
WebSocket socket;
46-
if (!string.IsNullOrEmpty(subProtocol))
47-
{
48-
socket = await context.WebSockets.AcceptWebSocketAsync(subProtocol);
49-
}
50-
else
51-
{
52-
socket = await context.WebSockets.AcceptWebSocketAsync();
53-
}
54-
55-
await ProcessWebSocketRequest(socket, replyWithPartialMessages, replyWithEnhancedCloseMessage);
26+
await WebSocketEchoHelper.RunEchoAll(
27+
socket, options.ReplyWithPartialMessages, options.ReplyWithEnhancedCloseMessage);
5628
}
5729
catch (Exception)
5830
{
5931
// We might want to log these exceptions. But for now we ignore them.
6032
}
6133
}
62-
63-
private static async Task ProcessWebSocketRequest(
64-
WebSocket socket,
65-
bool replyWithPartialMessages,
66-
bool replyWithEnhancedCloseMessage)
67-
{
68-
var receiveBuffer = new byte[MaxBufferSize];
69-
var throwAwayBuffer = new byte[MaxBufferSize];
70-
71-
// Stay in loop while websocket is open
72-
while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent)
73-
{
74-
var receiveResult = await socket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), CancellationToken.None);
75-
if (receiveResult.MessageType == WebSocketMessageType.Close)
76-
{
77-
if (receiveResult.CloseStatus == WebSocketCloseStatus.Empty)
78-
{
79-
await socket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None);
80-
}
81-
else
82-
{
83-
WebSocketCloseStatus closeStatus = receiveResult.CloseStatus.GetValueOrDefault();
84-
await socket.CloseAsync(
85-
closeStatus,
86-
replyWithEnhancedCloseMessage ?
87-
("Server received: " + (int)closeStatus + " " + receiveResult.CloseStatusDescription) :
88-
receiveResult.CloseStatusDescription,
89-
CancellationToken.None);
90-
}
91-
92-
continue;
93-
}
94-
95-
// Keep reading until we get an entire message.
96-
int offset = receiveResult.Count;
97-
while (receiveResult.EndOfMessage == false)
98-
{
99-
if (offset < MaxBufferSize)
100-
{
101-
receiveResult = await socket.ReceiveAsync(
102-
new ArraySegment<byte>(receiveBuffer, offset, MaxBufferSize - offset),
103-
CancellationToken.None);
104-
}
105-
else
106-
{
107-
receiveResult = await socket.ReceiveAsync(
108-
new ArraySegment<byte>(throwAwayBuffer),
109-
CancellationToken.None);
110-
}
111-
112-
offset += receiveResult.Count;
113-
}
114-
115-
// Close socket if the message was too big.
116-
if (offset > MaxBufferSize)
117-
{
118-
await socket.CloseAsync(
119-
WebSocketCloseStatus.MessageTooBig,
120-
String.Format("{0}: {1} > {2}", WebSocketCloseStatus.MessageTooBig.ToString(), offset, MaxBufferSize),
121-
CancellationToken.None);
122-
123-
continue;
124-
}
125-
126-
bool sendMessage = false;
127-
string receivedMessage = null;
128-
if (receiveResult.MessageType == WebSocketMessageType.Text)
129-
{
130-
receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset);
131-
if (receivedMessage == ".close")
132-
{
133-
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None);
134-
}
135-
else if (receivedMessage == ".shutdown")
136-
{
137-
await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None);
138-
}
139-
else if (receivedMessage == ".abort")
140-
{
141-
socket.Abort();
142-
}
143-
else if (receivedMessage == ".delay5sec")
144-
{
145-
await Task.Delay(5000);
146-
}
147-
else if (receivedMessage == ".receiveMessageAfterClose")
148-
{
149-
byte[] buffer = new byte[1024];
150-
string message = $"{receivedMessage} {DateTime.Now.ToString("HH:mm:ss")}";
151-
buffer = System.Text.Encoding.UTF8.GetBytes(message);
152-
await socket.SendAsync(
153-
new ArraySegment<byte>(buffer, 0, message.Length),
154-
WebSocketMessageType.Text,
155-
true,
156-
CancellationToken.None);
157-
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None);
158-
}
159-
else if (socket.State == WebSocketState.Open)
160-
{
161-
sendMessage = true;
162-
}
163-
}
164-
else
165-
{
166-
sendMessage = true;
167-
}
168-
169-
if (sendMessage)
170-
{
171-
await socket.SendAsync(
172-
new ArraySegment<byte>(receiveBuffer, 0, offset),
173-
receiveResult.MessageType,
174-
!replyWithPartialMessages,
175-
CancellationToken.None);
176-
}
177-
if (receivedMessage == ".closeafter")
178-
{
179-
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None);
180-
}
181-
else if (receivedMessage == ".shutdownafter")
182-
{
183-
await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None);
184-
}
185-
}
186-
}
18734
}
18835
}

0 commit comments

Comments
 (0)