Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Foundation/NSUrlSessionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,19 @@ void DidReceiveChallengeImpl (NSUrlSession session, NSUrlSessionTask task, NSUrl
var credential = new NSUrlCredential (identity, new SecCertificate [] { cert }, NSUrlCredentialPersistence.ForSession);
completionHandler (NSUrlSessionAuthChallengeDisposition.UseCredential, credential);
return;
} else if (!AppContext.TryGetSwitch ("Foundation.NSUrlSessionHandler.NoMissingCertificateHandling", out bool enabled) || !enabled) {
// The server requested a certificate, but we don't have one to provide. Fail the request with a meaningful exception
// that allows the developer to identify this, ask the user for a certificate, add it to the ClientCertificates collection
// and then re-try the request.
lock (inflight.Lock) {
inflight.Exception = new HttpRequestException ("An error occurred while sending the request.",
new WebException ("Error: Certificate Required",
new AuthenticationException ("Error: Certificate Required"),
WebExceptionStatus.SecureChannelFailure, null));
}
// We will still continue with a null credential, since some services uses optional client certificates and this will still let it succeed
completionHandler (NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, null!);
return;
}
}

Expand Down
149 changes: 149 additions & 0 deletions tests/monotouch-test/System.Net.Http/MessageHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Linq;
using System.IO;
Expand All @@ -17,6 +18,9 @@
using System.Text;
using Xamarin.Utils;

using Network;
using Security;

namespace MonoTests.System.Net.Http {
[TestFixture]
[Preserve (AllMembers = true)]
Expand Down Expand Up @@ -713,6 +717,151 @@ public void TestNSUrlSessionHandlerSendClientCertificate ()
}
}

[Test]
public void TestNSUrlSessionHandlerOptionalClientCertificate ()
{
NWListener? listener = null;
try {
listener = CreateNWTlsListener (requireClientCert: false);
var port = listener.Port;

var done = TestRuntime.TryRunAsync (TimeSpan.FromSeconds (30), async () => {
using var handler = new NSUrlSessionHandler ();
handler.TrustOverrideForUrl = (sender, url, trust) => true;
using var client = new HttpClient (handler);
var response = await client.GetAsync ($"https://localhost:{port}/");
response.EnsureSuccessStatusCode ();
}, out var ex);
Assert.IsTrue (done, "Request to localhost timed out.");
Assert.IsNull (ex, $"Exception wasn't expected, but got: {ex}");
} finally {
listener?.Cancel ();
listener?.Dispose ();
}
}

[Test]
public void TestNSUrlSessionHandlerDetectMissingClientCertificate ()
{
NWListener? listener = null;
try {
listener = CreateNWTlsListener (requireClientCert: true);
var port = listener.Port;

var done = TestRuntime.TryRunAsync (TimeSpan.FromSeconds (30), async () => {
using var handler = new NSUrlSessionHandler ();
handler.TrustOverrideForUrl = (sender, url, trust) => true;
using var client = new HttpClient (handler);
await client.GetAsync ($"https://localhost:{port}/");
}, out var ex);
Assert.IsTrue (done, "Request to localhost timed out.");
Assert.IsNotNull (ex, "Exception was expected.");
Assert.IsInstanceOf (typeof (HttpRequestException), ex, "Exception");
Assert.IsInstanceOf (typeof (WebException), ex!.InnerException, "InnerException Type");
Assert.That (((WebException) ex.InnerException!).Status, Is.EqualTo (WebExceptionStatus.SecureChannelFailure), "InnerException Status");
Assert.IsInstanceOf (typeof (AuthenticationException), ex.InnerException.InnerException, "InnerException.InnerException Type");
} finally {
listener?.Cancel ();
listener?.Dispose ();
}
}

[Test]
public void TestNSUrlSessionHandlerDetectMissingClientCertificateOptOut ()
{
AppContext.TryGetSwitch ("Foundation.NSUrlSessionHandler.NoMissingCertificateHandling", out var originalValue);
NWListener? listener = null;
try {
AppContext.SetSwitch ("Foundation.NSUrlSessionHandler.NoMissingCertificateHandling", true);
listener = CreateNWTlsListener (requireClientCert: true);
var port = listener.Port;

var done = TestRuntime.TryRunAsync (TimeSpan.FromSeconds (30), async () => {
using var handler = new NSUrlSessionHandler ();
handler.TrustOverrideForUrl = (sender, url, trust) => true;
using var client = new HttpClient (handler);
await client.GetAsync ($"https://localhost:{port}/");
}, out var ex);
Assert.IsTrue (done, "Request to localhost timed out.");
// With the opt-out switch enabled, the new specific exception is not thrown.
// Instead we get a generic connection error (no WebException/AuthenticationException chain).
Assert.IsNotNull (ex, "Exception was expected.");
Assert.IsInstanceOf (typeof (HttpRequestException), ex, "Exception");
if (ex!.InnerException is WebException we)
Assert.That (we.Status, Is.Not.EqualTo (WebExceptionStatus.SecureChannelFailure), "Should not be SecureChannelFailure");
} finally {
AppContext.SetSwitch ("Foundation.NSUrlSessionHandler.NoMissingCertificateHandling", originalValue);
listener?.Cancel ();
listener?.Dispose ();
}
}

static NWListener CreateNWTlsListener (bool requireClientCert)
{
using var serverCert = CreateSelfSignedServerCertificate ();
using var secIdentity = SecIdentity.Import (serverCert);
using var secIdentity2 = new SecIdentity2 (secIdentity);
var readyEvent = new ManualResetEventSlim (false);

var parameters = NWParameters.CreateSecureTcp (
configureTls: tlsOptions => {
var tls = (NWProtocolTlsOptions) tlsOptions;
var secOptions = tls.ProtocolOptions;
secOptions.SetLocalIdentity (secIdentity2);
secOptions.SetPeerAuthenticationRequired (requireClientCert);
});
using var localEndpoint = NWEndpoint.Create ("127.0.0.1", "0");
parameters.LocalEndpoint = localEndpoint;

var listener = NWListener.Create (parameters);
parameters.Dispose ();

listener.SetQueue (CoreFoundation.DispatchQueue.DefaultGlobalQueue);

listener.SetStateChangedHandler ((state, error) => {
if (state == NWListenerState.Ready)
readyEvent.Set ();
if (state == NWListenerState.Failed)
readyEvent.Set ();
});

listener.SetNewConnectionHandler (connection => {
connection.SetQueue (CoreFoundation.DispatchQueue.DefaultGlobalQueue);
connection.SetStateChangeHandler ((connState, connError) => {
if (connState == NWConnectionState.Ready) {
// Read the HTTP request (just consume it), then send a response
connection.ReceiveReadOnlyData (1, 4096, (data, context, isComplete, error) => {
var response = Encoding.UTF8.GetBytes ("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nOK");
connection.Send (response, NWContentContext.FinalMessage, true, sendError => {
connection.Cancel ();
});
});
}
});
connection.Start ();
});

listener.Start ();

if (!readyEvent.Wait (TimeSpan.FromSeconds (10)))
throw new TimeoutException ("NWListener did not become ready in time.");

return listener;
}

static X509Certificate2 CreateSelfSignedServerCertificate ()
{
using var rsa = RSA.Create (2048);
var certRequest = new CertificateRequest (
"CN=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var sanBuilder = new SubjectAlternativeNameBuilder ();
sanBuilder.AddIpAddress (IPAddress.Loopback);
sanBuilder.AddDnsName ("localhost");
certRequest.CertificateExtensions.Add (sanBuilder.Build ());
var cert = certRequest.CreateSelfSigned (DateTimeOffset.UtcNow.AddDays (-1), DateTimeOffset.UtcNow.AddYears (1));
return X509CertificateLoader.LoadPkcs12 (cert.Export (X509ContentType.Pfx), null);
}

[Test]
public void AssertDefaultValuesNSUrlSessionHandler ()
{
Expand Down
Loading