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
35 changes: 33 additions & 2 deletions src/Foundation/NSUrlSessionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ namespace System.Net.Http {
namespace Foundation {
#endif

public delegate bool NSUrlSessionHandlerTrustOverrideCallback (NSUrlSessionHandler sender, SecTrust trust);

// useful extensions for the class in order to set it in a header
static class NSHttpCookieExtensions
{
Expand Down Expand Up @@ -291,6 +293,18 @@ public ICredentials Credentials {
}
}

NSUrlSessionHandlerTrustOverrideCallback trustOverride;

public NSUrlSessionHandlerTrustOverrideCallback TrustOverride {
get {
return trustOverride;
}
set {
EnsureModifiability ();
trustOverride = value;
}
}

bool sentRequest;

internal void EnsureModifiability ()
Expand Down Expand Up @@ -655,7 +669,7 @@ public override void DidCompleteWithError (NSUrlSession session, NSUrlSessionTas
inflight.CancellationTokenSource.Cancel ();
inflight.Errored = true;

var exc = createExceptionForNSError (error);
var exc = inflight.Exception ?? createExceptionForNSError (error);
inflight.CompletionSource.TrySetException (exc);
inflight.Stream.TrySetException (exc);
} else {
Expand Down Expand Up @@ -705,6 +719,22 @@ public override void DidReceiveChallenge (NSUrlSession session, NSUrlSessionTask
if (inflight == null)
return;

// ToCToU for the callback
var trustCallback = sessionHandler.TrustOverride;
if (trustCallback != null && challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodServerTrust) {
if (trustCallback (sessionHandler, challenge.ProtectionSpace.ServerSecTrust)) {
var credential = new NSUrlCredential (challenge.ProtectionSpace.ServerSecTrust);
completionHandler (NSUrlSessionAuthChallengeDisposition.UseCredential, credential);
} else {
// user callback rejected the certificate, we want to set the exception, else the user will
// see as if the request was cancelled.
lock (inflight.Lock) {
inflight.Exception = new HttpRequestException ("An error occurred while sending the request.", new WebException ("Error: TrustFailure"));
}
completionHandler (NSUrlSessionAuthChallengeDisposition.CancelAuthenticationChallenge, null);
}
return;
}
// case for the basic auth failing up front. As per apple documentation:
// The URL Loading System is designed to handle various aspects of the HTTP protocol for you. As a result, you should not modify the following headers using
// the addValue(_:forHTTPHeaderField:) or setValue(_:forHTTPHeaderField:) methods:
Expand All @@ -717,7 +747,7 @@ public override void DidReceiveChallenge (NSUrlSession session, NSUrlSessionTask
// but we are hiding such a situation from our users, we can nevertheless know if the header was added and deal with it. The idea is as follows,
// check if we are in the first attempt, if we are (PreviousFailureCount == 0), we check the headers of the request and if we do have the Auth
// header, it means that we do not have the correct credentials, in any other case just do what it is expected.

if (challenge.PreviousFailureCount == 0) {
var authHeader = inflight.Request?.Headers?.Authorization;
if (!(string.IsNullOrEmpty (authHeader?.Scheme) && string.IsNullOrEmpty (authHeader?.Parameter))) {
Expand Down Expand Up @@ -783,6 +813,7 @@ class InflightData : IDisposable
public HttpRequestMessage Request { get; set; }
public HttpResponseMessage Response { get; set; }

public Exception Exception { get; set; }
public bool ResponseSent { get; set; }
public bool Errored { get; set; }
public bool Disposed { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion tests/introspection/ApiFrameworkTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ public bool Skip (string @namespace)
// not a framework, largely p/invokes to /usr/lib/libSystem.dylib
case "Darwin":
return true;
#endif
// not directly bindings
case "System.Net.Http":
return true;
#endif
default:
return false;
}
Expand Down
107 changes: 107 additions & 0 deletions tests/monotouch-test/System.Net.Http/MessageHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,112 @@ public void RedirectionWithAuthorizationHeaders (Type handlerType)
Assert.IsNull (ex, $"Exception {ex} for {json}");
}
}

#if !__WATCHOS__
[TestCase (typeof (HttpClientHandler))]
#endif
[TestCase (typeof (NSUrlSessionHandler))]
public void RejectSslCertificatesServicePointManager (Type handlerType)
{
TestRuntime.AssertSystemVersion (PlatformName.MacOSX, 10, 9, throwIfOtherPlatform: false);
TestRuntime.AssertSystemVersion (PlatformName.iOS, 7, 0, throwIfOtherPlatform: false);

bool servicePointManagerCbWasExcuted = false;
bool done = false;
Exception ex = null;

var handler = GetHandler (handlerType);
if (handler is NSUrlSessionHandler ns) {
ns.TrustOverride += (a,b) => {
servicePointManagerCbWasExcuted = true;
// return false, since we want to test that the exception is raised
return false;
};
} else {
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => {
servicePointManagerCbWasExcuted = true;
// return false, since we want to test that the exception is raised
return false;
};
}

TestRuntime.RunAsync (DateTime.Now.AddSeconds (30), async () =>
{
try {
HttpClient client = new HttpClient (handler);
client.BaseAddress = new Uri ("https://httpbin.org");
var byteArray = new UTF8Encoding ().GetBytes ("username:password");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue ("Basic", Convert.ToBase64String(byteArray));
var result = await client.GetAsync ("https://httpbin.org/redirect/3");
} catch (Exception e) {
ex = e;
} finally {
done = true;
ServicePointManager.ServerCertificateValidationCallback = null;
}
}, () => done);

if (!done) { // timeouts happen in the bost due to dns issues, connection issues etc.. we do not want to fail
Assert.Inconclusive ("Request timedout.");
} else {
// assert the exception type
Assert.IsInstanceOfType (typeof (HttpRequestException), ex);
Assert.IsNotNull (ex.InnerException);
Assert.IsInstanceOfType (typeof (WebException), ex.InnerException);
}
}

#if !__WATCHOS__
[TestCase (typeof (HttpClientHandler))]
#endif
[TestCase (typeof (NSUrlSessionHandler))]
public void AcceptSslCertificatesServicePointManager (Type handlerType)
{
TestRuntime.AssertSystemVersion (PlatformName.MacOSX, 10, 9, throwIfOtherPlatform: false);
TestRuntime.AssertSystemVersion (PlatformName.iOS, 7, 0, throwIfOtherPlatform: false);

bool servicePointManagerCbWasExcuted = false;
bool done = false;
Exception ex = null;

var handler = GetHandler (handlerType);
if (handler is NSUrlSessionHandler ns) {
ns.TrustOverride += (a,b) => {
servicePointManagerCbWasExcuted = true;
return true;
};
} else {
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => {
servicePointManagerCbWasExcuted = true;
return true;
};
}

TestRuntime.RunAsync (DateTime.Now.AddSeconds (30), async () =>
{
try {
HttpClient client = new HttpClient (handler);
client.BaseAddress = new Uri ("https://httpbin.org");
var byteArray = new UTF8Encoding ().GetBytes ("username:password");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue ("Basic", Convert.ToBase64String(byteArray));
var result = await client.GetAsync ("https://httpbin.org/redirect/3");
} catch (Exception e) {
ex = e;
} finally {
done = true;
ServicePointManager.ServerCertificateValidationCallback = null;
}
}, () => done);

if (!done) { // timeouts happen in the bost due to dns issues, connection issues etc.. we do not want to fail
Assert.Inconclusive ("Request timedout.");
} else {
// assert that we did not get an exception
if (ex != null && ex.InnerException != null) {
// we could get here.. if we have a diff issue, in that case, lets get the exception message and assert is not the trust issue
Assert.AreNotEqual (ex.InnerException.Message, "Error: TrustFailure");
}
}
}
}
}