Skip to content

Instantly share code, notes, and snippets.

@nberardi
Last active May 4, 2016 21:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nberardi/ab1b448cc4036122c4e50f7c137e4521 to your computer and use it in GitHub Desktop.
Save nberardi/ab1b448cc4036122c4e50f7c137e4521 to your computer and use it in GitHub Desktop.
A rethinking of the NSUrlSessionHandler.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
#if UNIFIED
using CoreFoundation;
using Foundation;
using Security;
#else
using MonoTouch.CoreFoundation;
using MonoTouch.Foundation;
using MonoTouch.Security;
using System.Globalization;
#endif
#if SYSTEM_NET_HTTP
namespace System.Net.Http
#else
namespace Foundation
#endif
{
public class NSUrlSessionHandler : HttpClientHandler
{
private readonly Dictionary<string, string> _headerSeparators = new Dictionary<string, string>
{
["User-Agent"] = " ",
["Server"] = " "
};
private readonly NSUrlSession _session;
private readonly Dictionary<NSUrlSessionTask, InflightData> _inflightRequests;
private readonly object _inflightRequestsLock = new object();
public NSUrlSessionHandler()
{
var configuration = NSUrlSessionConfiguration.DefaultSessionConfiguration;
// we cannot do a bitmask but we can set the minimum based on ServicePointManager.SecurityProtocol minimum
var sp = ServicePointManager.SecurityProtocol;
if ((sp & SecurityProtocolType.Ssl3) != 0)
configuration.TLSMinimumSupportedProtocol = SslProtocol.Ssl_3_0;
else if ((sp & SecurityProtocolType.Tls) != 0)
configuration.TLSMinimumSupportedProtocol = SslProtocol.Tls_1_0;
else if ((sp & SecurityProtocolType.Tls11) != 0)
configuration.TLSMinimumSupportedProtocol = SslProtocol.Tls_1_1;
else if ((sp & SecurityProtocolType.Tls12) != 0)
configuration.TLSMinimumSupportedProtocol = SslProtocol.Tls_1_2;
_session = NSUrlSession.FromConfiguration(NSUrlSessionConfiguration.DefaultSessionConfiguration, new NSUrlSessionHandlerDelegate(this), null);
_inflightRequests = new Dictionary<NSUrlSessionTask, InflightData>();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
private string GetHeaderSeparator(string name)
{
if (_headerSeparators.ContainsKey(name))
return _headerSeparators[name];
return ",";
}
private async Task<NSUrlRequest> CreateRequest(HttpRequestMessage request)
{
var stream = Stream.Null;
var headers = request.Headers as IEnumerable<KeyValuePair<string, IEnumerable<string>>>;
if (request.Content != null)
{
stream = await request.Content.ReadAsStreamAsync().ConfigureAwait(false);
headers = headers.Union(request.Content.Headers).ToArray();
}
var nsrequest = new NSMutableUrlRequest
{
AllowsCellularAccess = true,
CachePolicy = NSUrlRequestCachePolicy.UseProtocolCachePolicy,
HttpMethod = request.Method.ToString().ToUpperInvariant(),
Url = NSUrl.FromString(request.RequestUri.AbsoluteUri),
Headers = headers.Aggregate(new NSMutableDictionary(), (acc, x) =>
{
acc.Add(new NSString(x.Key), new NSString(string.Join(GetHeaderSeparator(x.Key), x.Value)));
return acc;
})
};
if (stream != Stream.Null)
nsrequest.BodyStream = new WrappedNSInputStream(stream);
return nsrequest;
}
#if SYSTEM_NET_HTTP || MONOMAC
internal
#endif
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var nsrequest = await CreateRequest(request);
var dataTask = _session.CreateDataTask(nsrequest);
var tcs = new TaskCompletionSource<HttpResponseMessage>();
cancellationToken.Register(() =>
{
dataTask.Cancel();
InflightData inflight;
lock (_inflightRequestsLock)
if (_inflightRequests.TryGetValue(dataTask, out inflight))
_inflightRequests.Remove(dataTask);
dataTask?.Dispose();
inflight?.Dispose();
tcs.TrySetCanceled();
});
lock (_inflightRequestsLock)
_inflightRequests.Add(dataTask, new InflightData
{
RequestUrl = request.RequestUri.AbsoluteUri,
CompletionSource = tcs,
CancellationToken = cancellationToken,
Stream = new NSUrlSessionDataTaskStream(),
Request = request
});
if (dataTask.State == NSUrlSessionTaskState.Suspended)
dataTask.Resume();
return await tcs.Task.ConfigureAwait(false);
}
#if MONOMAC
// Needed since we strip during linking since we're inside a product assembly.
[Preserve (AllMembers = true)]
#endif
private class NSUrlSessionHandlerDelegate : NSUrlSessionDataDelegate
{
private readonly NSUrlSessionHandler _handler;
public NSUrlSessionHandlerDelegate(NSUrlSessionHandler handler)
{
_handler = handler;
}
private InflightData GetInflightData(NSUrlSessionTask task)
{
var inflight = default(InflightData);
lock (_handler._inflightRequestsLock)
if (_handler._inflightRequests.TryGetValue(task, out inflight))
return inflight;
return null;
}
private void RemoveInflightData(NSUrlSessionTask task)
{
InflightData inflight;
lock (_handler._inflightRequestsLock)
if (_handler._inflightRequests.TryGetValue(task, out inflight))
_handler._inflightRequests.Remove(task);
task?.Dispose();
inflight?.Dispose();
}
public override void DidReceiveResponse(NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action<NSUrlSessionResponseDisposition> completionHandler)
{
var inflight = GetInflightData(dataTask);
try
{
var urlResponse = (NSHttpUrlResponse)response;
var status = (int)urlResponse.StatusCode;
var content = new NSUrlSessionDataTaskStreamContent(inflight.Stream, () =>
{
dataTask.Cancel();
inflight.Disposed = true;
inflight.Stream.TrySetException(new ObjectDisposedException("The content stream was disposed."));
RemoveInflightData(dataTask);
});
// NB: The double cast is because of a Xamarin compiler bug
var httpResponse = new HttpResponseMessage((HttpStatusCode)status)
{
Content = content,
RequestMessage = inflight.Request
};
httpResponse.RequestMessage.RequestUri = new Uri(urlResponse.Url.AbsoluteString);
foreach (var v in urlResponse.AllHeaderFields)
{
// NB: Cocoa trolling us so hard by giving us back dummy dictionary entries
if (v.Key == null || v.Value == null) continue;
httpResponse.Headers.TryAddWithoutValidation(v.Key.ToString(), v.Value.ToString());
httpResponse.Content.Headers.TryAddWithoutValidation(v.Key.ToString(), v.Value.ToString());
}
inflight.Response = httpResponse;
// We don't want to send the response back to the task just yet. Because we want to mimic .NET behavior
// as much as possible. When the response is sent back in .NET, the content stream is ready to read or the
// request has completed, because of this we want to send back the response in DidReceiveData or DidCompleteWithError
if (dataTask.State == NSUrlSessionTaskState.Suspended)
dataTask.Resume();
}
catch (Exception ex)
{
inflight.CompletionSource.TrySetException(ex);
inflight.Stream.TrySetException(ex);
dataTask.Cancel();
RemoveInflightData(dataTask);
}
completionHandler(NSUrlSessionResponseDisposition.Allow);
}
public override void DidReceiveData(NSUrlSession session, NSUrlSessionDataTask dataTask, NSData data)
{
var inflight = GetInflightData(dataTask);
inflight.Stream.Add(data);
SetResponse(inflight);
}
public override void DidCompleteWithError(NSUrlSession session, NSUrlSessionTask task, NSError error)
{
var inflight = GetInflightData(task);
// this can happen if the HTTP request times out and it is removed as part of the cancelation process
if (inflight != null)
{
// set the stream as finished
inflight.Stream.TrySetReceivedAllData();
// send the error or send the response back
if (error != null)
{
inflight.Errored = true;
var exc = CreateExceptionForNSError(error);
inflight.CompletionSource.TrySetException(exc);
inflight.Stream.TrySetException(exc);
}
else
{
inflight.Completed = true;
SetResponse(inflight);
}
RemoveInflightData(task);
}
}
private void SetResponse(InflightData inflight)
{
lock (inflight.Lock)
{
if (inflight.ResponseSent)
return;
if (inflight.CompletionSource.Task.IsCompleted)
return;
var httpResponse = inflight.Response;
inflight.ResponseSent = true;
// EVIL HACK: having TrySetResult inline was blocking the request from completing
Task.Run(() => inflight.CompletionSource.TrySetResult(httpResponse));
}
}
public override void DidReceiveChallenge(NSUrlSession session, NSUrlSessionTask task, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler)
{
if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodNTLM)
{
NetworkCredential credentialsToUse;
if (_handler.Credentials != null)
{
if (_handler.Credentials is NetworkCredential)
{
credentialsToUse = (NetworkCredential)_handler.Credentials;
}
else
{
var inflight = GetInflightData(task);
var uri = inflight.Request.RequestUri;
credentialsToUse = _handler.Credentials.GetCredential(uri, "NTLM");
}
var credential = new NSUrlCredential(credentialsToUse.UserName, credentialsToUse.Password, NSUrlCredentialPersistence.ForSession);
completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, credential);
}
return;
}
}
public override void WillCacheResponse(NSUrlSession session, NSUrlSessionDataTask dataTask, NSCachedUrlResponse proposedResponse, Action<NSCachedUrlResponse> completionHandler)
{
// never cache
completionHandler(null);
}
public override void WillPerformHttpRedirection(NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action<NSUrlRequest> completionHandler)
{
var nextRequest = (_handler.AllowAutoRedirect ? newRequest : null);
completionHandler(nextRequest);
}
private static Exception CreateExceptionForNSError(NSError error)
{
var ret = default(Exception);
var webExceptionStatus = WebExceptionStatus.UnknownError;
var innerException = new NSErrorException(error);
if (error.Domain == NSError.NSUrlErrorDomain)
{
// Convert the error code into an enumeration (this is future
// proof, rather than just casting integer)
CFNetworkErrors urlError;
if (!Enum.TryParse(error.Code.ToString(), out urlError))
urlError = CFNetworkErrors.Unknown;
// Parse the enum into a web exception status or exception. Some
// of these values don't necessarily translate completely to
// what WebExceptionStatus supports, so made some best guesses
// here. For your reading pleasure, compare these:
//
// Apple docs: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Constants/index.html#//apple_ref/doc/constant_group/URL_Loading_System_Error_Codes
// .NET docs: http://msdn.microsoft.com/en-us/library/system.net.webexceptionstatus(v=vs.110).aspx
switch (urlError)
{
case CFNetworkErrors.Cancelled:
case CFNetworkErrors.UserCancelledAuthentication:
// No more processing is required so just return.
return new OperationCanceledException(error.LocalizedDescription, innerException);
case CFNetworkErrors.BadURL:
case CFNetworkErrors.UnsupportedURL:
case CFNetworkErrors.CannotConnectToHost:
case CFNetworkErrors.ResourceUnavailable:
case CFNetworkErrors.NotConnectedToInternet:
case CFNetworkErrors.UserAuthenticationRequired:
case CFNetworkErrors.InternationalRoamingOff:
case CFNetworkErrors.CallIsActive:
case CFNetworkErrors.DataNotAllowed:
webExceptionStatus = WebExceptionStatus.ConnectFailure;
break;
case CFNetworkErrors.TimedOut:
webExceptionStatus = WebExceptionStatus.Timeout;
break;
case CFNetworkErrors.CannotFindHost:
case CFNetworkErrors.DNSLookupFailed:
webExceptionStatus = WebExceptionStatus.NameResolutionFailure;
break;
case CFNetworkErrors.DataLengthExceedsMaximum:
webExceptionStatus = WebExceptionStatus.MessageLengthLimitExceeded;
break;
case CFNetworkErrors.NetworkConnectionLost:
webExceptionStatus = WebExceptionStatus.ConnectionClosed;
break;
case CFNetworkErrors.HTTPTooManyRedirects:
case CFNetworkErrors.RedirectToNonExistentLocation:
webExceptionStatus = WebExceptionStatus.ProtocolError;
break;
case CFNetworkErrors.RequestBodyStreamExhausted:
webExceptionStatus = WebExceptionStatus.SendFailure;
break;
case CFNetworkErrors.BadServerResponse:
case CFNetworkErrors.ZeroByteResource:
case CFNetworkErrors.CannotDecodeRawData:
case CFNetworkErrors.CannotDecodeContentData:
case CFNetworkErrors.CannotParseResponse:
case CFNetworkErrors.FileDoesNotExist:
case CFNetworkErrors.FileIsDirectory:
case CFNetworkErrors.NoPermissionsToReadFile:
case CFNetworkErrors.CannotLoadFromNetwork:
case CFNetworkErrors.CannotCreateFile:
case CFNetworkErrors.CannotOpenFile:
case CFNetworkErrors.CannotCloseFile:
case CFNetworkErrors.CannotWriteToFile:
case CFNetworkErrors.CannotRemoveFile:
case CFNetworkErrors.CannotMoveFile:
case CFNetworkErrors.DownloadDecodingFailedMidStream:
case CFNetworkErrors.DownloadDecodingFailedToComplete:
webExceptionStatus = WebExceptionStatus.ReceiveFailure;
break;
case CFNetworkErrors.SecureConnectionFailed:
webExceptionStatus = WebExceptionStatus.SecureChannelFailure;
break;
case CFNetworkErrors.ServerCertificateHasBadDate:
case CFNetworkErrors.ServerCertificateHasUnknownRoot:
case CFNetworkErrors.ServerCertificateNotYetValid:
case CFNetworkErrors.ServerCertificateUntrusted:
case CFNetworkErrors.ClientCertificateRejected:
case CFNetworkErrors.ClientCertificateRequired:
webExceptionStatus = WebExceptionStatus.TrustFailure;
break;
}
}
// Always create a WebException so that it can be handled by the client.
ret = new WebException(error.LocalizedDescription, innerException, webExceptionStatus, response: null);
return ret;
}
}
#if MONOMAC
// Needed since we strip during linking since we're inside a product assembly.
[Preserve (AllMembers = true)]
#endif
private class InflightData : IDisposable
{
public readonly object Lock = new object();
public string RequestUrl { get; set; }
public TaskCompletionSource<HttpResponseMessage> CompletionSource { get; set; }
public CancellationToken CancellationToken { get; set; }
public NSUrlSessionDataTaskStream Stream { get; set; }
public HttpRequestMessage Request { get; set; }
public HttpResponseMessage Response { get; set; }
public bool ResponseSent { get; set; }
public bool Errored { get; set; }
public bool Disposed { get; set; }
public bool Completed { get; set; }
public bool Done { get { return Errored || Disposed || Completed || CancellationToken.IsCancellationRequested; } }
public void Dispose()
{
Stream?.Dispose();
Request?.Dispose();
Response?.Dispose();
}
}
#if MONOMAC
// Needed since we strip during linking since we're inside a product assembly.
[Preserve (AllMembers = true)]
#endif
private class NSUrlSessionDataTaskStreamContent : StreamContent
{
private Action _onDisposed;
public NSUrlSessionDataTaskStreamContent(NSUrlSessionDataTaskStream source, Action onDisposed) : base(source)
{
_onDisposed = onDisposed;
}
protected override void Dispose(bool disposing)
{
var action = Interlocked.Exchange(ref _onDisposed, null);
action?.Invoke();
base.Dispose(disposing);
}
}
#if MONOMAC
// Needed since we strip during linking since we're inside a product assembly.
[Preserve (AllMembers = true)]
#endif
private class NSUrlSessionDataTaskStream : Stream
{
private readonly Queue<NSData> _data;
private readonly object _dataLock = new object();
private long _position;
private long _length;
private bool _receivedAllData;
private Exception _exc;
private NSData _current;
private Stream _currentStream;
public NSUrlSessionDataTaskStream()
{
_data = new Queue<NSData>();
}
protected override void Dispose(bool disposing)
{
foreach (var q in _data)
q?.Dispose();
base.Dispose(disposing);
}
public void Add(NSData data)
{
lock (_dataLock)
{
_data.Enqueue(data);
_length += (int)data.Length;
}
}
public void TrySetReceivedAllData()
{
_receivedAllData = true;
}
public void TrySetException(Exception exc)
{
_exc = exc;
TrySetReceivedAllData();
}
private void ThrowIfNeeded(CancellationToken cancellationToken)
{
if (_exc != null)
throw _exc;
cancellationToken.ThrowIfCancellationRequested();
}
public override int Read(byte[] buffer, int offset, int count)
{
return ReadAsync(buffer, offset, count).Result;
}
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
// try to throw on enter
ThrowIfNeeded(cancellationToken);
while (_current == null)
{
lock (_dataLock)
{
if (_data.Count == 0 && _receivedAllData && _position == _length)
return 0;
if (_data.Count > 0 && _current == null)
{
_current = _data.Peek();
_currentStream = _current.AsStream();
break;
}
}
await Task.Delay(50);
}
// try to throw again before read
ThrowIfNeeded(cancellationToken);
var d = _currentStream;
var bufferCount = Math.Min(count, (int)(d.Length - d.Position));
var bytesRead = await d.ReadAsync(buffer, offset, bufferCount, cancellationToken);
// add the bytes read from the pointer to the position
_position += bytesRead;
// remove the current primary reference if the current position has reached the end of the bytes
if (d.Position == d.Length)
{
lock (_dataLock)
{
// this is the same object, it was done to make the cleanup
_data.Dequeue();
_current?.Dispose();
_currentStream?.Dispose();
_current = null;
_currentStream = null;
}
}
return bytesRead;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override bool CanTimeout => false;
public override long Length => _length;
public override void SetLength(long value)
{
throw new InvalidOperationException();
}
public override long Position
{
get { return _position; }
set { throw new InvalidOperationException(); }
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new InvalidOperationException();
}
public override void Flush()
{
throw new InvalidOperationException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new InvalidOperationException();
}
}
#if MONOMAC
// Needed since we strip during linking since we're inside a product assembly.
[Preserve (AllMembers = true)]
#endif
private class WrappedNSInputStream : NSInputStream
{
private NSStreamStatus _status;
private readonly Stream _stream;
public WrappedNSInputStream(Stream stream)
{
_status = NSStreamStatus.NotOpen;
_stream = stream;
}
public override NSStreamStatus Status
{
get
{
return _status;
}
}
public override void Open()
{
_status = NSStreamStatus.Open;
Notify(CFStreamEventType.OpenCompleted);
}
public override void Close()
{
_status = NSStreamStatus.Closed;
}
public override nint Read(IntPtr buffer, nuint len)
{
var source = new byte[len];
var read = _stream.Read(source, 0, (int)len);
Marshal.Copy(source, 0, buffer, (int)len);
//if (read == 0)
// Notify(CFStreamEventType.EndEncountered);
return read;
}
public override bool HasBytesAvailable()
{
return true;
}
protected override bool GetBuffer(out IntPtr buffer, out nuint len)
{
// Just call the base implemention (which will return false)
return base.GetBuffer(out buffer, out len);
}
protected override bool SetCFClientFlags(CFStreamEventType inFlags, IntPtr inCallback, IntPtr inContextPtr)
{
// Just call the base implementation, which knows how to handle everything.
return base.SetCFClientFlags(inFlags, inCallback, inContextPtr);
}
bool notifying;
[Export("_scheduleInCFRunLoop:forMode:")]
public void ScheduleInCFRunLoop(CFRunLoop runloop, NSString mode)
{
if (notifying)
return;
notifying = true;
Notify(CFStreamEventType.HasBytesAvailable);
notifying = false;
}
[Export("_unscheduleFromCFRunLoop:forMode:")]
public void UnscheduleInCFRunLoop(CFRunLoop runloop, NSString mode)
{
// Nothing to do here
}
protected override void Dispose(bool disposing)
{
_stream?.Dispose();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment