Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit c008bb5

Browse files
authored
Expose ClientWebSocketOptions.RemoteCertificateValidationCallback (#28141)
* Expose ClientWebSocketOptions.RemoteCertificateValidationCallback * Address PR feedback * Address PR feedback
1 parent d402c95 commit c008bb5

File tree

9 files changed

+120
-47
lines changed

9 files changed

+120
-47
lines changed

src/System.Net.WebSockets.Client/ref/System.Net.WebSockets.Client.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ internal ClientWebSocketOptions() { }
3232
public System.Net.ICredentials Credentials { get { throw null; } set { } }
3333
public System.TimeSpan KeepAliveInterval { get { throw null; } set { } }
3434
public System.Net.IWebProxy Proxy { get { throw null; } set { } }
35+
public System.Net.Security.RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get { throw null; } set { } }
3536
public bool UseDefaultCredentials { get { throw null; } set { } }
3637
public void AddSubProtocol(string subProtocol) { }
3738
public void SetBuffer(int receiveBufferSize, int sendBufferSize) { }

src/System.Net.WebSockets.Client/ref/System.Net.WebSockets.Client.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
</ItemGroup>
1414
<ItemGroup>
1515
<ProjectReference Include="..\..\System.Net.Primitives\ref\System.Net.Primitives.csproj" />
16+
<ProjectReference Include="..\..\System.Net.Security\ref\System.Net.Security.csproj" />
1617
<ProjectReference Include="..\..\System.Net.WebSockets\ref\System.Net.WebSockets.csproj" />
1718
<ProjectReference Include="..\..\System.Runtime\ref\System.Runtime.csproj" />
1819
<ProjectReference Include="..\..\System.Security.Cryptography.X509Certificates\ref\System.Security.Cryptography.X509Certificates.csproj" />

src/System.Net.WebSockets.Client/src/ILLinkTrim.xml

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/System.Net.WebSockets.Client/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,7 @@
183183
<data name="net_WebSockets_UWPClientCertSupportRequiresCertInPersonalCertificateStore" xml:space="preserve">
184184
<value>Client certificate was not found in the personal (\"MY\") certificate store. In UWP, client certificates are only supported if they have been added to that certificate store.</value>
185185
</data>
186+
<data name="net_WebSockets_RemoteValidationCallbackNotSupported" xml:space="preserve">
187+
<value>ClientWebSocketOptions.RemoteCertificateValidationCallback is not supported on this platform.</value>
188+
</data>
186189
</root>

src/System.Net.WebSockets.Client/src/System/Net/WebSockets/ClientWebSocketOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public X509CertificateCollection ClientCertificates
109109
}
110110
}
111111

112-
internal RemoteCertificateValidationCallback RemoteCertificateValidationCallback // TODO #12038: Expose publicly.
112+
public RemoteCertificateValidationCallback RemoteCertificateValidationCallback
113113
{
114114
get => _remoteCertificateValidationCallback;
115115
set

src/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.WinRT.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ private WebSocketHandle(WinRTWebSocket webSocket)
3939

4040
public async Task ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
4141
{
42+
if (options.RemoteCertificateValidationCallback != null)
43+
{
44+
throw new PlatformNotSupportedException(SR.net_WebSockets_RemoteValidationCallbackNotSupported);
45+
}
46+
4247
try
4348
{
4449
await _webSocket.ConnectAsync(uri, cancellationToken, options).ConfigureAwait(false);

src/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -142,42 +142,5 @@ public static void KeepAliveInterval_Roundtrips()
142142

143143
AssertExtensions.Throws<ArgumentOutOfRangeException>("value", () => cws.Options.KeepAliveInterval = TimeSpan.MinValue);
144144
}
145-
146-
[OuterLoop("Connects to remote service")]
147-
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "Lacks RemoteCertificateValidationCallback to enable loopback testing")]
148-
[ConditionalFact(nameof(WebSocketsSupported), nameof(ClientCertificatesSupported))]
149-
public async Task ClientCertificates_ValidCertificate_ServerReceivesCertificateAndConnectAsyncSucceeds()
150-
{
151-
if (PlatformDetection.IsWindows7)
152-
{
153-
return; // [ActiveIssue(27846)]
154-
}
155-
156-
using (X509Certificate2 clientCert = Test.Common.Configuration.Certificates.GetClientCertificate())
157-
{
158-
await LoopbackServer.CreateClientAndServerAsync(async uri =>
159-
{
160-
using (var clientSocket = new ClientWebSocket())
161-
using (var cts = new CancellationTokenSource(TimeOutMilliseconds))
162-
{
163-
clientSocket.Options.ClientCertificates.Add(clientCert);
164-
clientSocket.Options.GetType().GetProperty("RemoteCertificateValidationCallback", BindingFlags.NonPublic | BindingFlags.Instance)
165-
.SetValue(clientSocket.Options, new RemoteCertificateValidationCallback(delegate { return true; })); // TODO: #12038: Simplify once property is public.
166-
await clientSocket.ConnectAsync(uri, cts.Token);
167-
}
168-
}, server => server.AcceptConnectionAsync(async connection =>
169-
{
170-
// Validate that the client certificate received by the server matches the one configured on
171-
// the client-side socket.
172-
SslStream sslStream = Assert.IsType<SslStream>(connection.Stream);
173-
Assert.NotNull(sslStream.RemoteCertificate);
174-
Assert.Equal(clientCert, new X509Certificate2(sslStream.RemoteCertificate));
175-
176-
// Complete the WebSocket upgrade over the secure channel. After this is done, the client-side
177-
// ConnectAsync should complete.
178-
Assert.True(await LoopbackHelper.WebSocketHandshakeAsync(connection));
179-
}), new LoopbackServer.Options { UseSsl = true, WebSocketEndpoint = true });
180-
}
181-
}
182145
}
183146
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Net.Security;
6+
using System.Net.Test.Common;
7+
using System.Reflection;
8+
using System.Security.Cryptography.X509Certificates;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
12+
using Xunit;
13+
using Xunit.Abstractions;
14+
15+
namespace System.Net.WebSockets.Client.Tests
16+
{
17+
public partial class ClientWebSocketOptionsTests : ClientWebSocketTestBase
18+
{
19+
[ConditionalFact(nameof(WebSocketsSupported), nameof(ClientCertificatesSupported))]
20+
public void RemoteCertificateValidationCallback_Roundtrips()
21+
{
22+
using (var cws = new ClientWebSocket())
23+
{
24+
Assert.Null(cws.Options.RemoteCertificateValidationCallback);
25+
26+
RemoteCertificateValidationCallback callback = delegate { return true; };
27+
cws.Options.RemoteCertificateValidationCallback = callback;
28+
Assert.Same(callback, cws.Options.RemoteCertificateValidationCallback);
29+
30+
cws.Options.RemoteCertificateValidationCallback = null;
31+
Assert.Null(cws.Options.RemoteCertificateValidationCallback);
32+
}
33+
}
34+
35+
[OuterLoop("Connects to remote service")]
36+
[ConditionalTheory(nameof(WebSocketsSupported), nameof(ClientCertificatesSupported))]
37+
[InlineData(false)]
38+
[InlineData(true)]
39+
public async Task RemoteCertificateValidationCallback_PassedRemoteCertificateInfo(bool secure)
40+
{
41+
if (PlatformDetection.IsWindows7)
42+
{
43+
return; // [ActiveIssue(27846)]
44+
}
45+
46+
bool callbackInvoked = false;
47+
48+
await LoopbackServer.CreateClientAndServerAsync(async uri =>
49+
{
50+
using (var cws = new ClientWebSocket())
51+
using (var cts = new CancellationTokenSource(TimeOutMilliseconds))
52+
{
53+
cws.Options.RemoteCertificateValidationCallback = (source, cert, chain, errors) =>
54+
{
55+
Assert.NotNull(source);
56+
Assert.NotNull(cert);
57+
Assert.NotNull(chain);
58+
Assert.NotEqual(SslPolicyErrors.None, errors);
59+
callbackInvoked = true;
60+
return true;
61+
};
62+
await cws.ConnectAsync(uri, cts.Token);
63+
}
64+
}, server => server.AcceptConnectionAsync(async connection =>
65+
{
66+
Assert.True(await LoopbackHelper.WebSocketHandshakeAsync(connection));
67+
}),
68+
new LoopbackServer.Options { UseSsl = secure, WebSocketEndpoint = true });
69+
70+
Assert.Equal(secure, callbackInvoked);
71+
}
72+
73+
[OuterLoop("Connects to remote service")]
74+
[ConditionalFact(nameof(WebSocketsSupported), nameof(ClientCertificatesSupported))]
75+
public async Task ClientCertificates_ValidCertificate_ServerReceivesCertificateAndConnectAsyncSucceeds()
76+
{
77+
if (PlatformDetection.IsWindows7)
78+
{
79+
return; // [ActiveIssue(27846)]
80+
}
81+
82+
using (X509Certificate2 clientCert = Test.Common.Configuration.Certificates.GetClientCertificate())
83+
{
84+
await LoopbackServer.CreateClientAndServerAsync(async uri =>
85+
{
86+
using (var clientSocket = new ClientWebSocket())
87+
using (var cts = new CancellationTokenSource(TimeOutMilliseconds))
88+
{
89+
clientSocket.Options.ClientCertificates.Add(clientCert);
90+
clientSocket.Options.RemoteCertificateValidationCallback = delegate { return true; };
91+
await clientSocket.ConnectAsync(uri, cts.Token);
92+
}
93+
}, server => server.AcceptConnectionAsync(async connection =>
94+
{
95+
// Validate that the client certificate received by the server matches the one configured on
96+
// the client-side socket.
97+
SslStream sslStream = Assert.IsType<SslStream>(connection.Stream);
98+
Assert.NotNull(sslStream.RemoteCertificate);
99+
Assert.Equal(clientCert, new X509Certificate2(sslStream.RemoteCertificate));
100+
101+
// Complete the WebSocket upgrade over the secure channel. After this is done, the client-side
102+
// ConnectAsync should complete.
103+
Assert.True(await LoopbackHelper.WebSocketHandshakeAsync(connection));
104+
}), new LoopbackServer.Options { UseSsl = true, WebSocketEndpoint = true });
105+
}
106+
}
107+
}
108+
}

src/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<Compile Include="AbortTest.cs" />
4646
<Compile Include="CancelTest.cs" />
4747
<Compile Include="ClientWebSocketOptionsTests.cs" />
48+
<Compile Include="ClientWebSocketOptionsTests.netcoreapp.cs" Condition="'$(TargetGroup)' == 'netcoreapp'" />
4849
<Compile Include="ClientWebSocketTestBase.cs" />
4950
<Compile Include="ClientWebSocketUnitTest.cs" />
5051
<Compile Include="CloseTest.cs" />

0 commit comments

Comments
 (0)