| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859 |
- //
- // NSUrlSessionHandler.cs:
- //
- // Authors:
- // Paul Betts <[email protected]>
- // Nick Berardi <[email protected]>
- //
- // 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.
- //
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using System.IO;
- using System.Linq;
- using System.Net;
- using System.Net.Http;
- using System.Net.Http.Headers;
- using System.Runtime.InteropServices;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Text;
- #if UNIFIED
- using CoreFoundation;
- using Foundation;
- using Security;
- #else
- using MonoTouch.CoreFoundation;
- using MonoTouch.Foundation;
- using MonoTouch.Security;
- using System.Globalization;
- using nint = System.Int32;
- using nuint = System.UInt32;
- #endif
- #if !MONOMAC
- using UIKit;
- #endif
- #if SYSTEM_NET_HTTP
- namespace System.Net.Http {
- #else
- namespace Foundation {
- #endif
- // useful extensions for the class in order to set it in a header
- static class NSHttpCookieExtensions
- {
- static void AppendSegment(StringBuilder builder, string name, string value)
- {
- if (builder.Length > 0)
- builder.Append ("; ");
- builder.Append (name);
- if (value != null)
- builder.Append ("=").Append (value);
- }
- // returns the header for a cookie
- public static string GetHeaderValue (this NSHttpCookie cookie)
- {
- var header = new StringBuilder();
- AppendSegment (header, cookie.Name, cookie.Value);
- AppendSegment (header, NSHttpCookie.KeyPath.ToString (), cookie.Path.ToString ());
- AppendSegment (header, NSHttpCookie.KeyDomain.ToString (), cookie.Domain.ToString ());
- AppendSegment (header, NSHttpCookie.KeyVersion.ToString (), cookie.Version.ToString ());
- if (cookie.Comment != null)
- AppendSegment (header, NSHttpCookie.KeyComment.ToString (), cookie.Comment.ToString());
- if (cookie.CommentUrl != null)
- AppendSegment (header, NSHttpCookie.KeyCommentUrl.ToString (), cookie.CommentUrl.ToString());
- if (cookie.Properties.ContainsKey (NSHttpCookie.KeyDiscard))
- AppendSegment (header, NSHttpCookie.KeyDiscard.ToString (), null);
- if (cookie.ExpiresDate != null) {
- // Format according to RFC1123; 'r' uses invariant info (DateTimeFormatInfo.InvariantInfo)
- var dateStr = ((DateTime) cookie.ExpiresDate).ToUniversalTime ().ToString("r", CultureInfo.InvariantCulture);
- AppendSegment (header, NSHttpCookie.KeyExpires.ToString (), dateStr);
- }
- if (cookie.Properties.ContainsKey (NSHttpCookie.KeyMaximumAge)) {
- var timeStampString = (NSString) cookie.Properties[NSHttpCookie.KeyMaximumAge];
- AppendSegment (header, NSHttpCookie.KeyMaximumAge.ToString (), timeStampString);
- }
- if (cookie.IsSecure)
- AppendSegment (header, NSHttpCookie.KeySecure.ToString(), null);
- if (cookie.IsHttpOnly)
- AppendSegment (header, "httponly", null); // Apple does not show the key for the httponly
- return header.ToString ();
- }
- }
- public partial class NSUrlSessionHandler : HttpMessageHandler
- {
- private const string SetCookie = "Set-Cookie";
- readonly Dictionary<string, string> headerSeparators = new Dictionary<string, string> {
- ["User-Agent"] = " ",
- ["Server"] = " "
- };
- readonly NSUrlSession session;
- readonly Dictionary<NSUrlSessionTask, InflightData> inflightRequests;
- readonly object inflightRequestsLock = new object ();
- #if !MONOMAC && !MONOTOUCH_WATCH
- readonly bool isBackgroundSession = false;
- NSObject notificationToken; // needed to make sure we do not hang if not using a background session
- #endif
- static NSUrlSessionConfiguration CreateConfig ()
- {
- // modifying the configuration does not affect future calls
- var config = NSUrlSessionConfiguration.DefaultSessionConfiguration;
- // but we want, by default, the timeout from HttpClient to have precedence over the one from NSUrlSession
- // Double.MaxValue does not work, so default to 24 hours
- config.TimeoutIntervalForRequest = 24 * 60 * 60;
- config.TimeoutIntervalForResource = 24 * 60 * 60;
- return config;
- }
- public NSUrlSessionHandler () : this (CreateConfig ())
- {
- }
- [CLSCompliant (false)]
- public NSUrlSessionHandler (NSUrlSessionConfiguration configuration)
- {
- if (configuration == null)
- throw new ArgumentNullException (nameof (configuration));
- #if !MONOMAC && !MONOTOUCH_WATCH
- // if the configuration has an identifier, we are dealing with a background session,
- // therefore, we do not have to listen to the notifications.
- isBackgroundSession = !string.IsNullOrEmpty (configuration.Identifier);
- #endif
- AllowAutoRedirect = true;
- // 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 (configuration, (INSUrlSessionDelegate) new NSUrlSessionHandlerDelegate (this), null);
- inflightRequests = new Dictionary<NSUrlSessionTask, InflightData> ();
- }
- #if !MONOMAC && !MONOTOUCH_WATCH
- void AddNotification ()
- {
- if (!isBackgroundSession && notificationToken == null)
- notificationToken = NSNotificationCenter.DefaultCenter.AddObserver (UIApplication.WillResignActiveNotification, BackgroundNotificationCb);
- }
- void RemoveNotification ()
- {
- if (notificationToken != null) {
- NSNotificationCenter.DefaultCenter.RemoveObserver (notificationToken);
- notificationToken = null;
- }
- }
- void BackgroundNotificationCb (NSNotification obj)
- {
- // we do not need to call the lock, we call cancel on the source, that will trigger all the needed code to
- // clean the resources and such
- foreach (var r in inflightRequests.Values) {
- r.CompletionSource.TrySetCanceled ();
- }
- }
- #endif
- public long MaxInputInMemory { get; set; } = long.MaxValue;
- void RemoveInflightData (NSUrlSessionTask task, bool cancel = true)
- {
- lock (inflightRequestsLock) {
- inflightRequests.Remove (task);
- #if !MONOMAC && !MONOTOUCH_WATCH
- // do we need to be notified? If we have not inflightData, we do not
- if (inflightRequests.Count == 0)
- RemoveNotification ();
- #endif
- }
- if (cancel)
- task?.Cancel ();
- task?.Dispose ();
- }
- protected override void Dispose (bool disposing)
- {
- #if !MONOMAC && !MONOTOUCH_WATCH
- // remove the notification if present, method checks against null
- RemoveNotification ();
- #endif
- lock (inflightRequestsLock) {
- foreach (var pair in inflightRequests) {
- pair.Key?.Cancel ();
- pair.Key?.Dispose ();
- }
- inflightRequests.Clear ();
- }
- base.Dispose (disposing);
- }
- bool disableCaching;
- public bool DisableCaching {
- get {
- return disableCaching;
- }
- set {
- EnsureModifiability ();
- disableCaching = value;
- }
- }
- string GetHeaderSeparator (string name)
- {
- string value;
- if (!headerSeparators.TryGetValue (name, out value))
- value = ",";
- return value;
- }
- 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 = System.Linq.Enumerable.ToArray(headers.Union (request.Content.Headers));
- }
- 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) {
- // HttpContent.TryComputeLength is `protected internal` :-( but it's indirectly called by headers
- var length = request.Content.Headers.ContentLength;
- if (length.HasValue && (length <= MaxInputInMemory))
- nsrequest.Body = NSData.FromStream (stream);
- else
- nsrequest.BodyStream = new WrappedNSInputStream (stream);
- }
- return nsrequest;
- }
- #if SYSTEM_NET_HTTP || MONOMAC
- internal
- #endif
- protected override async Task<HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
- {
- Volatile.Write (ref sentRequest, true);
- var nsrequest = await CreateRequest (request).ConfigureAwait(false);
- var dataTask = session.CreateDataTask (nsrequest);
- var tcs = new TaskCompletionSource<HttpResponseMessage> ();
- lock (inflightRequestsLock) {
- #if !MONOMAC && !MONOTOUCH_WATCH
- // Add the notification whenever needed
- AddNotification ();
- #endif
- 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 ();
- // as per documentation:
- // If this token is already in the canceled state, the
- // delegate will be run immediately and synchronously.
- // Any exception the delegate generates will be
- // propagated out of this method call.
- //
- // The execution of the register ensures that if we
- // receive a already cancelled token or it is cancelled
- // just before this call, we will cancel the task.
- // Other approaches are harder, since querying the state
- // of the token does not guarantee that in the next
- // execution a threads cancels it.
- cancellationToken.Register (() => {
- RemoveInflightData (dataTask);
- tcs.TrySetCanceled ();
- });
- 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
- partial class NSUrlSessionHandlerDelegate : NSUrlSessionDataDelegate
- {
- readonly NSUrlSessionHandler sessionHandler;
- public NSUrlSessionHandlerDelegate (NSUrlSessionHandler handler)
- {
- sessionHandler = handler;
- }
- InflightData GetInflightData (NSUrlSessionTask task)
- {
- var inflight = default (InflightData);
- lock (sessionHandler.inflightRequestsLock)
- if (sessionHandler.inflightRequests.TryGetValue (task, out inflight)) {
- // ensure that we did not cancel the request, if we did, do cancel the task
- if (inflight.CancellationToken.IsCancellationRequested)
- task?.Cancel ();
- return inflight;
- }
- // if we did not manage to get the inflight data, we either got an error or have been canceled, lets cancel the task, that will execute DidCompleteWithError
- task?.Cancel ();
- return null;
- }
- public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action<NSUrlSessionResponseDisposition> completionHandler)
- {
- var inflight = GetInflightData (dataTask);
- if (inflight == null)
- return;
- try {
- var urlResponse = (NSHttpUrlResponse)response;
- var status = (int)urlResponse.StatusCode;
- var content = new NSUrlSessionDataTaskStreamContent (inflight.Stream, () => {
- if (!inflight.Completed) {
- dataTask.Cancel ();
- }
- inflight.Disposed = true;
- inflight.Stream.TrySetException (new ObjectDisposedException ("The content stream was disposed."));
- sessionHandler.RemoveInflightData (dataTask);
- }, inflight.CancellationToken);
- // 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;
- // NSUrlSession tries to be smart with cookies, we will not use the raw value but the ones provided by the cookie storage
- if (v.Key.ToString () == SetCookie) continue;
- httpResponse.Headers.TryAddWithoutValidation (v.Key.ToString (), v.Value.ToString ());
- httpResponse.Content.Headers.TryAddWithoutValidation (v.Key.ToString (), v.Value.ToString ());
- }
- var cookies = session.Configuration.HttpCookieStorage.CookiesForUrl (response.Url);
- for (var index = 0; index < cookies.Length; index++) {
- httpResponse.Headers.TryAddWithoutValidation (SetCookie, cookies [index].GetHeaderValue ());
- }
- 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 DidReceiveData (NSUrlSession session, NSUrlSessionDataTask dataTask, NSData data)
- {
- var inflight = GetInflightData (dataTask);
- if (inflight == null)
- return;
- 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 cancellation 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);
- }
- }
- 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 WillCacheResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSCachedUrlResponse proposedResponse, Action<NSCachedUrlResponse> completionHandler)
- {
- completionHandler (sessionHandler.DisableCaching ? null : proposedResponse);
- }
- public override void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action<NSUrlRequest> completionHandler)
- {
- completionHandler (sessionHandler.AllowAutoRedirect ? newRequest : null);
- }
- public override void DidReceiveChallenge (NSUrlSession session, NSUrlSessionTask task, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler)
- {
- var inflight = GetInflightData (task);
- if (inflight == 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:
- // 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 = inflight.Request?.Headers?.Authorization;
- if (!(string.IsNullOrEmpty (authHeader?.Scheme) && string.IsNullOrEmpty (authHeader?.Parameter))) {
- completionHandler (NSUrlSessionAuthChallengeDisposition.RejectProtectionSpace, null);
- return;
- }
- }
- if (sessionHandler.Credentials != null && TryGetAuthenticationType (challenge.ProtectionSpace, out string authType)) {
- NetworkCredential credentialsToUse = null;
- if (authType != RejectProtectionSpaceAuthType) {
- var uri = inflight.Request.RequestUri;
- credentialsToUse = sessionHandler.Credentials.GetCredential (uri, authType);
- }
- if (credentialsToUse != null) {
- var credential = new NSUrlCredential (credentialsToUse.UserName, credentialsToUse.Password, NSUrlCredentialPersistence.ForSession);
- completionHandler (NSUrlSessionAuthChallengeDisposition.UseCredential, credential);
- } else {
- // Rejecting the challenge allows the next authentication method in the request to be delivered to
- // the DidReceiveChallenge method. Another authentication method may have credentials available.
- completionHandler (NSUrlSessionAuthChallengeDisposition.RejectProtectionSpace, null);
- }
- } else {
- completionHandler (NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, challenge.ProposedCredential);
- }
- }
- static readonly string RejectProtectionSpaceAuthType = "reject";
- static bool TryGetAuthenticationType (NSUrlProtectionSpace protectionSpace, out string authenticationType)
- {
- if (protectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodNTLM) {
- authenticationType = "NTLM";
- } else if (protectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodHTTPBasic) {
- authenticationType = "basic";
- } else if (protectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodNegotiate ||
- protectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodHTMLForm ||
- protectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodHTTPDigest) {
- // Want to reject this authentication type to allow the next authentication method in the request to
- // be used.
- authenticationType = RejectProtectionSpaceAuthType;
- } else {
- // ServerTrust, ClientCertificate or Default.
- authenticationType = null;
- return false;
- }
- return true;
- }
- }
- #if MONOMAC
- // Needed since we strip during linking since we're inside a product assembly.
- [Preserve (AllMembers = true)]
- #endif
- class InflightData
- {
- 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; } }
- }
- #if MONOMAC
- // Needed since we strip during linking since we're inside a product assembly.
- [Preserve (AllMembers = true)]
- #endif
- class NSUrlSessionDataTaskStreamContent : StreamContent
- {
- Action disposed;
- public NSUrlSessionDataTaskStreamContent (NSUrlSessionDataTaskStream source, Action onDisposed, CancellationToken token) : base (source, token)
- {
- disposed = onDisposed;
- }
- protected override void Dispose (bool disposing)
- {
- var action = Interlocked.Exchange (ref disposed, 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
- class NSUrlSessionDataTaskStream : Stream
- {
- readonly Queue<NSData> data;
- readonly object dataLock = new object ();
- long position;
- long length;
- bool receivedAllData;
- Exception exc;
- NSData current;
- Stream currentStream;
- public NSUrlSessionDataTaskStream ()
- {
- data = new Queue<NSData> ();
- }
- public void Add (NSData d)
- {
- lock (dataLock) {
- data.Enqueue (d);
- length += (int)d.Length;
- }
- }
- public void TrySetReceivedAllData ()
- {
- receivedAllData = true;
- }
- public void TrySetException (Exception e)
- {
- exc = e;
- TrySetReceivedAllData ();
- }
- 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).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 ();
- currentStream?.Dispose ();
- // We cannot use current?.Dispose. The reason is the following one:
- // In the DidReceiveResponse, if iOS realizes that a buffer can be reused,
- // because the data is the same, it will do so. Such a situation does happen
- // between requests, that is, request A and request B will get the same NSData
- // (buffer) in the delegate. In this case, we cannot dispose the NSData because
- // it might be that a different request received it and it is present in
- // its NSUrlSessionDataTaskStream stream. We can only trust the gc to do the job
- // which is better than copying the data over.
- 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
- class WrappedNSInputStream : NSInputStream
- {
- NSStreamStatus status;
- CFRunLoopSource source;
- readonly Stream stream;
- bool notifying;
- public WrappedNSInputStream (Stream inputStream)
- {
- status = NSStreamStatus.NotOpen;
- stream = inputStream;
- source = new CFRunLoopSource (Handle);
- }
- public override NSStreamStatus Status => 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 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 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);
- }
- // 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 SetProperty (NSObject property, NSString key)
- {
- return false;
- }
- 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);
- }
- 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 ();
- }
- }
- }
- }
|