Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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));
}
Comment thread
rolfbjarne marked this conversation as resolved.
// We will still continue with a null credential, since some services use optional client certificates and this will still let it succeed
completionHandler (NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, challenge.ProposedCredential);
return;
}
}

Expand Down
154 changes: 154 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,156 @@ 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)
{
var (pfxData, pfxPassword) = CreateSelfSignedServerCertificatePfx ();
using var secIdentity = SecIdentity.Import (pfxData, pfxPassword);
using var secIdentity2 = new SecIdentity2 (secIdentity);
using var readyEvent = new ManualResetEventSlim (false);
NWError? listenerError = null;

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.Failed)
listenerError = error;
if (state == NWListenerState.Ready || state == NWListenerState.Failed)
readyEvent.Set ();
});
Comment thread
rolfbjarne marked this conversation as resolved.

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.");

if (listenerError is not null)
throw new InvalidOperationException ($"NWListener failed to start: {listenerError}");

return listener;
}

static (byte [] Data, string Password) CreateSelfSignedServerCertificatePfx ()
{
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));
var password = Guid.NewGuid ().ToString ();
return (cert.Export (X509ContentType.Pfx, password), password);
}

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