// // NSUrlSessionHandler.cs: // // Authors: // Paul Betts // Nick Berardi // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #define UNIFIED 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; using CoreFoundation; using Foundation; using Security; namespace Xamarin.SSLPinning.iOS { public partial class NSUrlSessionHandler : HttpMessageHandler { private readonly Dictionary headerSeparators = new Dictionary { ["User-Agent"] = " ", ["Server"] = " " }; private readonly Dictionary inflightRequests; private readonly object inflightRequestsLock = new object(); private readonly NSUrlSession session; private bool allowAutoRedirect; private ICredentials credentials; private bool disableCaching; private bool sentRequest; public NSUrlSessionHandler() { AllowAutoRedirect = true; 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(); } public bool AllowAutoRedirect { get { return allowAutoRedirect; } set { EnsureModifiability(); allowAutoRedirect = value; } } public ICredentials Credentials { get { return credentials; } set { EnsureModifiability(); credentials = value; } } public bool DisableCaching { get { return disableCaching; } set { EnsureModifiability(); disableCaching = value; } } public NSData UntrustedCertificate { get; set; } internal void EnsureModifiability() { if (sentRequest) throw new InvalidOperationException( "This instance has already started one or more requests. " + "Properties can only be modified before sending the first request."); } protected override void Dispose(bool disposing) { lock (inflightRequestsLock) { foreach (var pair in inflightRequests) { pair.Key?.Cancel(); pair.Key?.Dispose(); } inflightRequests.Clear(); } base.Dispose(disposing); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var nsrequest = await CreateRequest(request).ConfigureAwait(false); var dataTask = session.CreateDataTask(nsrequest); var tcs = new TaskCompletionSource(); cancellationToken.Register(() => { RemoveInflightData(dataTask); 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); } // almost identical to ModernHttpClient version but it uses the constants from monotouch.dll | Xamarin.[iOS|WatchOS|TVOS].dll private static Exception createExceptionForNSError(NSError error) { var webExceptionStatus = WebExceptionStatus.UnknownError; var innerException = new NSErrorException(error); // errors that exists in both share the same error code, so we can use a single switch/case // this also ease watchOS integration as if does not expose CFNetwork but (I would not be // surprised if it)could return some of it's error codes #if MONOTOUCH_WATCH if (error.Domain == NSError.NSUrlErrorDomain) { #else if ((error.Domain == NSError.NSUrlErrorDomain) || (error.Domain == NSError.CFNetworkErrorDomain)) { #endif // 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 ((NSUrlError)(long)error.Code) { case NSUrlError.Cancelled: case NSUrlError.UserCancelledAuthentication: #if !MONOTOUCH_WATCH case (NSUrlError)NSNetServicesStatus.CancelledError: #endif // No more processing is required so just return. return new OperationCanceledException(error.LocalizedDescription, innerException); case NSUrlError.BadURL: case NSUrlError.UnsupportedURL: case NSUrlError.CannotConnectToHost: case NSUrlError.ResourceUnavailable: case NSUrlError.NotConnectedToInternet: case NSUrlError.UserAuthenticationRequired: case NSUrlError.InternationalRoamingOff: case NSUrlError.CallIsActive: case NSUrlError.DataNotAllowed: #if !MONOTOUCH_WATCH case (NSUrlError)CFNetworkErrors.Socks5BadCredentials: case (NSUrlError)CFNetworkErrors.Socks5UnsupportedNegotiationMethod: case (NSUrlError)CFNetworkErrors.Socks5NoAcceptableMethod: case (NSUrlError)CFNetworkErrors.HttpAuthenticationTypeUnsupported: case (NSUrlError)CFNetworkErrors.HttpBadCredentials: case (NSUrlError)CFNetworkErrors.HttpBadURL: #endif webExceptionStatus = WebExceptionStatus.ConnectFailure; break; case NSUrlError.TimedOut: #if !MONOTOUCH_WATCH case (NSUrlError)CFNetworkErrors.NetServiceTimeout: #endif webExceptionStatus = WebExceptionStatus.Timeout; break; case NSUrlError.CannotFindHost: case NSUrlError.DNSLookupFailed: #if !MONOTOUCH_WATCH case (NSUrlError)CFNetworkErrors.HostNotFound: case (NSUrlError)CFNetworkErrors.NetServiceDnsServiceFailure: #endif webExceptionStatus = WebExceptionStatus.NameResolutionFailure; break; case NSUrlError.DataLengthExceedsMaximum: webExceptionStatus = WebExceptionStatus.MessageLengthLimitExceeded; break; case NSUrlError.NetworkConnectionLost: #if !MONOTOUCH_WATCH case (NSUrlError)CFNetworkErrors.HttpConnectionLost: #endif webExceptionStatus = WebExceptionStatus.ConnectionClosed; break; case NSUrlError.HTTPTooManyRedirects: case NSUrlError.RedirectToNonExistentLocation: #if !MONOTOUCH_WATCH case (NSUrlError)CFNetworkErrors.HttpRedirectionLoopDetected: #endif webExceptionStatus = WebExceptionStatus.ProtocolError; break; case NSUrlError.RequestBodyStreamExhausted: #if !MONOTOUCH_WATCH case (NSUrlError)CFNetworkErrors.SocksUnknownClientVersion: case (NSUrlError)CFNetworkErrors.SocksUnsupportedServerVersion: case (NSUrlError)CFNetworkErrors.HttpParseFailure: #endif webExceptionStatus = WebExceptionStatus.SendFailure; break; case NSUrlError.BadServerResponse: case NSUrlError.ZeroByteResource: case NSUrlError.CannotDecodeRawData: case NSUrlError.CannotDecodeContentData: case NSUrlError.CannotParseResponse: case NSUrlError.FileDoesNotExist: case NSUrlError.FileIsDirectory: case NSUrlError.NoPermissionsToReadFile: case NSUrlError.CannotLoadFromNetwork: case NSUrlError.CannotCreateFile: case NSUrlError.CannotOpenFile: case NSUrlError.CannotCloseFile: case NSUrlError.CannotWriteToFile: case NSUrlError.CannotRemoveFile: case NSUrlError.CannotMoveFile: case NSUrlError.DownloadDecodingFailedMidStream: case NSUrlError.DownloadDecodingFailedToComplete: #if !MONOTOUCH_WATCH case (NSUrlError)CFNetworkErrors.Socks4RequestFailed: case (NSUrlError)CFNetworkErrors.Socks4IdentdFailed: case (NSUrlError)CFNetworkErrors.Socks4IdConflict: case (NSUrlError)CFNetworkErrors.Socks4UnknownStatusCode: case (NSUrlError)CFNetworkErrors.Socks5BadState: case (NSUrlError)CFNetworkErrors.Socks5BadResponseAddr: case (NSUrlError)CFNetworkErrors.CannotParseCookieFile: case (NSUrlError)CFNetworkErrors.NetServiceUnknown: case (NSUrlError)CFNetworkErrors.NetServiceCollision: case (NSUrlError)CFNetworkErrors.NetServiceNotFound: case (NSUrlError)CFNetworkErrors.NetServiceInProgress: case (NSUrlError)CFNetworkErrors.NetServiceBadArgument: case (NSUrlError)CFNetworkErrors.NetServiceInvalid: #endif webExceptionStatus = WebExceptionStatus.ReceiveFailure; break; case NSUrlError.SecureConnectionFailed: webExceptionStatus = WebExceptionStatus.SecureChannelFailure; break; case NSUrlError.ServerCertificateHasBadDate: case NSUrlError.ServerCertificateHasUnknownRoot: case NSUrlError.ServerCertificateNotYetValid: case NSUrlError.ServerCertificateUntrusted: case NSUrlError.ClientCertificateRejected: case NSUrlError.ClientCertificateRequired: webExceptionStatus = WebExceptionStatus.TrustFailure; break; #if !MONOTOUCH_WATCH case (NSUrlError)CFNetworkErrors.HttpProxyConnectionFailure: case (NSUrlError)CFNetworkErrors.HttpBadProxyCredentials: case (NSUrlError)CFNetworkErrors.PacFileError: case (NSUrlError)CFNetworkErrors.PacFileAuth: case (NSUrlError)CFNetworkErrors.HttpsProxyConnectionFailure: case (NSUrlError)CFNetworkErrors.HttpsProxyFailureUnexpectedResponseToConnectMethod: webExceptionStatus = WebExceptionStatus.RequestProhibitedByProxy; break; #endif } } // Always create a WebException so that it can be handled by the client. return new WebException(error.LocalizedDescription, innerException); //, webExceptionStatus, response: null); } private async Task CreateRequest(HttpRequestMessage request) { var stream = Stream.Null; var headers = request.Headers as IEnumerable>>; 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 = DisableCaching ? NSUrlRequestCachePolicy.ReloadIgnoringCacheData : 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; } private string GetHeaderSeparator(string name) { string value; if (!headerSeparators.TryGetValue(name, out value)) value = ","; return value; } private void RemoveInflightData(NSUrlSessionTask task, bool cancel = true) { InflightData inflight; lock (inflightRequestsLock) if (inflightRequests.TryGetValue(task, out inflight)) inflightRequests.Remove(task); if (cancel) task?.Cancel(); task?.Dispose(); } #if SYSTEM_NET_HTTP || MONOMAC internal #endif #if MONOMAC // Needed since we strip during linking since we're inside a product assembly. [Preserve (AllMembers = true)] #endif private class InflightData { public readonly object Lock = new object(); public CancellationToken CancellationToken { get; set; } public bool Completed { get; set; } public TaskCompletionSource CompletionSource { get; set; } public bool Disposed { get; set; } public bool Done { get { return Errored || Disposed || Completed || CancellationToken.IsCancellationRequested; } } public bool Errored { get; set; } public HttpRequestMessage Request { get; set; } public string RequestUrl { get; set; } public HttpResponseMessage Response { get; set; } public bool ResponseSent { get; set; } public NSUrlSessionDataTaskStream Stream { get; set; } } private class NSUrlSessionDataTaskStream : Stream { private readonly Queue data; private readonly object dataLock = new object(); private NSData current; private Stream currentStream; private Exception exc; private long length; private long position; private bool receivedAllData; public NSUrlSessionDataTaskStream() { data = new Queue(); } public override bool CanRead => true; public override bool CanSeek => false; public override bool CanTimeout => false; public override bool CanWrite => false; public override long Length => length; public override long Position { get { return position; } set { throw new InvalidOperationException(); } } public void Add(NSData d) { lock (dataLock) { data.Enqueue(d); length += (int)d.Length; } } public override void Flush() { throw new InvalidOperationException(); } public override int Read(byte[] buffer, int offset, int count) { return ReadAsync(buffer, offset, count).Result; } public override async Task 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).ConfigureAwait(false); } // 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).ConfigureAwait(false); // 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 long Seek(long offset, SeekOrigin origin) { throw new InvalidOperationException(); } public override void SetLength(long value) { throw new InvalidOperationException(); } public void TrySetException(Exception e) { exc = e; TrySetReceivedAllData(); } public void TrySetReceivedAllData() { receivedAllData = true; } public override void Write(byte[] buffer, int offset, int count) { throw new InvalidOperationException(); } protected override void Dispose(bool disposing) { lock (dataLock) { foreach (var q in data) q?.Dispose(); } base.Dispose(disposing); } private void ThrowIfNeeded(CancellationToken cancellationToken) { if (exc != null) throw exc; cancellationToken.ThrowIfCancellationRequested(); } } private class NSUrlSessionDataTaskStreamContent : StreamContent { private Action disposed; public NSUrlSessionDataTaskStreamContent(NSUrlSessionDataTaskStream source, Action onDisposed) : base(source) { disposed = onDisposed; } protected override void Dispose(bool disposing) { var action = Interlocked.Exchange(ref disposed, null); action?.Invoke(); base.Dispose(disposing); } } partial class NSUrlSessionHandlerDelegate : NSUrlSessionDataDelegate { private readonly NSUrlSessionHandler sessionHandler; public NSUrlSessionHandlerDelegate(NSUrlSessionHandler handler) { sessionHandler = handler; } 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); } sessionHandler.RemoveInflightData(task, cancel: false); } } public override void DidReceiveChallenge(NSUrlSession session, NSUrlSessionTask task, NSUrlAuthenticationChallenge challenge, Action completionHandler) { // 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: // Authorization // Connection // Host // Proxy-Authenticate // Proxy-Authorization // WWW-Authenticate // 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 = GetInflightData(task)?.Request?.Headers?.Authorization; if (!(string.IsNullOrEmpty(authHeader?.Scheme) && string.IsNullOrEmpty(authHeader?.Parameter))) { completionHandler(NSUrlSessionAuthChallengeDisposition.RejectProtectionSpace, null); return; } } if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodNTLM) { if (sessionHandler.Credentials != null) { var credentialsToUse = sessionHandler.Credentials as NetworkCredential; if (credentialsToUse == null) { var uri = GetInflightData(task).Request.RequestUri; credentialsToUse = sessionHandler.Credentials.GetCredential(uri, "NTLM"); } var credential = new NSUrlCredential(credentialsToUse.UserName, credentialsToUse.Password, NSUrlCredentialPersistence.ForSession); completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, credential); } return; } ////NOTE: SSL Pinning here //var serverCertChain = challenge.ProtectionSpace.ServerSecTrust; //var first = serverCertChain[0].DerData; //var cert = NSData.FromFile("httpbin.cer"); if (sessionHandler.UntrustedCertificate != null)//first.IsEqual(cert)) { var trust = challenge.ProtectionSpace.ServerSecTrust; var rootCaData = sessionHandler.UntrustedCertificate; var x = new SecCertificate(rootCaData); trust.SetAnchorCertificates(new[] { x }); trust.SetAnchorCertificatesOnly(false); completionHandler(NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, challenge.ProposedCredential); } else { completionHandler(NSUrlSessionAuthChallengeDisposition.CancelAuthenticationChallenge, null); } } public override void DidReceiveData(NSUrlSession session, NSUrlSessionDataTask dataTask, NSData data) { var inflight = GetInflightData(dataTask); inflight.Stream.Add(data); SetResponse(inflight); } public override void DidReceiveResponse(NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action completionHandler) { var inflight = GetInflightData(dataTask); try { var urlResponse = (NSHttpUrlResponse)response; var status = (int)urlResponse.StatusCode; var content = new NSUrlSessionDataTaskStreamContent(inflight.Stream, () => { inflight.Disposed = true; inflight.Stream.TrySetException(new ObjectDisposedException("The content stream was disposed.")); sessionHandler.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); sessionHandler.RemoveInflightData(dataTask); } completionHandler(NSUrlSessionResponseDisposition.Allow); } public override void WillCacheResponse(NSUrlSession session, NSUrlSessionDataTask dataTask, NSCachedUrlResponse proposedResponse, Action completionHandler) { completionHandler(sessionHandler.DisableCaching ? null : proposedResponse); } public override void WillPerformHttpRedirection(NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action completionHandler) { completionHandler(sessionHandler.AllowAutoRedirect ? newRequest : null); } private InflightData GetInflightData(NSUrlSessionTask task) { var inflight = default(InflightData); lock (sessionHandler.inflightRequestsLock) if (sessionHandler.inflightRequests.TryGetValue(task, out inflight)) return inflight; return null; } 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)); } } } #if MONOMAC // Needed since we strip during linking since we're inside a product assembly. [Preserve (AllMembers = true)] #endif #if MONOMAC // Needed since we strip during linking since we're inside a product assembly. [Preserve (AllMembers = true)] #endif #if MONOMAC // Needed since we strip during linking since we're inside a product assembly. [Preserve (AllMembers = true)] #endif #if MONOMAC // Needed since we strip during linking since we're inside a product assembly. [Preserve (AllMembers = true)] #endif private class WrappedNSInputStream : NSInputStream { private readonly Stream stream; private bool notifying; private CFRunLoopSource source; private NSStreamStatus status; public WrappedNSInputStream(Stream inputStream) { status = NSStreamStatus.NotOpen; stream = inputStream; source = new CFRunLoopSource(Handle); } public override NSStreamStatus Status => status; public override void Close() { status = NSStreamStatus.Closed; } public override bool HasBytesAvailable() { return true; } public override void Open() { status = NSStreamStatus.Open; Notify(CFStreamEventType.OpenCompleted); } public override nint Read(IntPtr buffer, nuint len) { var sourceBytes = new byte[len]; var read = stream.Read(sourceBytes, 0, (int)len); Marshal.Copy(sourceBytes, 0, buffer, (int)len); if (notifying) return read; notifying = true; if (stream.CanSeek && stream.Position == stream.Length) { Notify(CFStreamEventType.EndEncountered); status = NSStreamStatus.AtEnd; } notifying = false; return read; } public override void Schedule(NSRunLoop aRunLoop, string mode) { var cfRunLoop = aRunLoop.GetCFRunLoop(); var nsMode = new NSString(mode); cfRunLoop.AddSource(source, nsMode); if (notifying) return; notifying = true; Notify(CFStreamEventType.HasBytesAvailable); notifying = false; } public override void Unschedule(NSRunLoop aRunLoop, string mode) { var cfRunLoop = aRunLoop.GetCFRunLoop(); var nsMode = new NSString(mode); cfRunLoop.RemoveSource(source, nsMode); } protected override void Dispose(bool disposing) { stream?.Dispose(); } 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); } // NSInvalidArgumentException Reason: *** -propertyForKey: only defined for abstract class. Define -[System_Net_Http_NSUrlSessionHandler_WrappedNSInputStream propertyForKey:]! protected override NSObject GetProperty(NSString key) { return null; } 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); } protected override bool SetProperty(NSObject property, NSString key) { return false; } } } }