NSUrlSessionHandler.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  1. //
  2. // NSUrlSessionHandler.cs:
  3. //
  4. // Authors:
  5. // Paul Betts <[email protected]>
  6. // Nick Berardi <[email protected]>
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining
  9. // a copy of this software and associated documentation files (the
  10. // "Software"), to deal in the Software without restriction, including
  11. // without limitation the rights to use, copy, modify, merge, publish,
  12. // distribute, sublicense, and/or sell copies of the Software, and to
  13. // permit persons to whom the Software is furnished to do so, subject to
  14. // the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be
  17. // included in all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  20. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  21. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  22. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  23. // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  24. // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  25. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  26. //
  27. using System;
  28. using System.Collections.Generic;
  29. using System.Globalization;
  30. using System.IO;
  31. using System.Linq;
  32. using System.Net;
  33. using System.Net.Http;
  34. using System.Net.Http.Headers;
  35. using System.Runtime.InteropServices;
  36. using System.Threading;
  37. using System.Threading.Tasks;
  38. using System.Text;
  39. #if UNIFIED
  40. using CoreFoundation;
  41. using Foundation;
  42. using Security;
  43. #else
  44. using MonoTouch.CoreFoundation;
  45. using MonoTouch.Foundation;
  46. using MonoTouch.Security;
  47. using System.Globalization;
  48. using nint = System.Int32;
  49. using nuint = System.UInt32;
  50. #endif
  51. #if !MONOMAC
  52. using UIKit;
  53. #endif
  54. #if SYSTEM_NET_HTTP
  55. namespace System.Net.Http {
  56. #else
  57. namespace Foundation {
  58. #endif
  59. // useful extensions for the class in order to set it in a header
  60. static class NSHttpCookieExtensions
  61. {
  62. static void AppendSegment(StringBuilder builder, string name, string value)
  63. {
  64. if (builder.Length > 0)
  65. builder.Append ("; ");
  66. builder.Append (name);
  67. if (value != null)
  68. builder.Append ("=").Append (value);
  69. }
  70. // returns the header for a cookie
  71. public static string GetHeaderValue (this NSHttpCookie cookie)
  72. {
  73. var header = new StringBuilder();
  74. AppendSegment (header, cookie.Name, cookie.Value);
  75. AppendSegment (header, NSHttpCookie.KeyPath.ToString (), cookie.Path.ToString ());
  76. AppendSegment (header, NSHttpCookie.KeyDomain.ToString (), cookie.Domain.ToString ());
  77. AppendSegment (header, NSHttpCookie.KeyVersion.ToString (), cookie.Version.ToString ());
  78. if (cookie.Comment != null)
  79. AppendSegment (header, NSHttpCookie.KeyComment.ToString (), cookie.Comment.ToString());
  80. if (cookie.CommentUrl != null)
  81. AppendSegment (header, NSHttpCookie.KeyCommentUrl.ToString (), cookie.CommentUrl.ToString());
  82. if (cookie.Properties.ContainsKey (NSHttpCookie.KeyDiscard))
  83. AppendSegment (header, NSHttpCookie.KeyDiscard.ToString (), null);
  84. if (cookie.ExpiresDate != null) {
  85. // Format according to RFC1123; 'r' uses invariant info (DateTimeFormatInfo.InvariantInfo)
  86. var dateStr = ((DateTime) cookie.ExpiresDate).ToUniversalTime ().ToString("r", CultureInfo.InvariantCulture);
  87. AppendSegment (header, NSHttpCookie.KeyExpires.ToString (), dateStr);
  88. }
  89. if (cookie.Properties.ContainsKey (NSHttpCookie.KeyMaximumAge)) {
  90. var timeStampString = (NSString) cookie.Properties[NSHttpCookie.KeyMaximumAge];
  91. AppendSegment (header, NSHttpCookie.KeyMaximumAge.ToString (), timeStampString);
  92. }
  93. if (cookie.IsSecure)
  94. AppendSegment (header, NSHttpCookie.KeySecure.ToString(), null);
  95. if (cookie.IsHttpOnly)
  96. AppendSegment (header, "httponly", null); // Apple does not show the key for the httponly
  97. return header.ToString ();
  98. }
  99. }
  100. public partial class NSUrlSessionHandler : HttpMessageHandler
  101. {
  102. private const string SetCookie = "Set-Cookie";
  103. readonly Dictionary<string, string> headerSeparators = new Dictionary<string, string> {
  104. ["User-Agent"] = " ",
  105. ["Server"] = " "
  106. };
  107. readonly NSUrlSession session;
  108. readonly Dictionary<NSUrlSessionTask, InflightData> inflightRequests;
  109. readonly object inflightRequestsLock = new object ();
  110. #if !MONOMAC && !MONOTOUCH_WATCH
  111. readonly bool isBackgroundSession = false;
  112. NSObject notificationToken; // needed to make sure we do not hang if not using a background session
  113. #endif
  114. static NSUrlSessionConfiguration CreateConfig ()
  115. {
  116. // modifying the configuration does not affect future calls
  117. var config = NSUrlSessionConfiguration.DefaultSessionConfiguration;
  118. // but we want, by default, the timeout from HttpClient to have precedence over the one from NSUrlSession
  119. // Double.MaxValue does not work, so default to 24 hours
  120. config.TimeoutIntervalForRequest = 24 * 60 * 60;
  121. config.TimeoutIntervalForResource = 24 * 60 * 60;
  122. return config;
  123. }
  124. public NSUrlSessionHandler () : this (CreateConfig ())
  125. {
  126. }
  127. [CLSCompliant (false)]
  128. public NSUrlSessionHandler (NSUrlSessionConfiguration configuration)
  129. {
  130. if (configuration == null)
  131. throw new ArgumentNullException (nameof (configuration));
  132. #if !MONOMAC && !MONOTOUCH_WATCH
  133. // if the configuration has an identifier, we are dealing with a background session,
  134. // therefore, we do not have to listen to the notifications.
  135. isBackgroundSession = !string.IsNullOrEmpty (configuration.Identifier);
  136. #endif
  137. AllowAutoRedirect = true;
  138. // we cannot do a bitmask but we can set the minimum based on ServicePointManager.SecurityProtocol minimum
  139. var sp = ServicePointManager.SecurityProtocol;
  140. if ((sp & SecurityProtocolType.Ssl3) != 0)
  141. configuration.TLSMinimumSupportedProtocol = SslProtocol.Ssl_3_0;
  142. else if ((sp & SecurityProtocolType.Tls) != 0)
  143. configuration.TLSMinimumSupportedProtocol = SslProtocol.Tls_1_0;
  144. else if ((sp & SecurityProtocolType.Tls11) != 0)
  145. configuration.TLSMinimumSupportedProtocol = SslProtocol.Tls_1_1;
  146. else if ((sp & SecurityProtocolType.Tls12) != 0)
  147. configuration.TLSMinimumSupportedProtocol = SslProtocol.Tls_1_2;
  148. session = NSUrlSession.FromConfiguration (configuration, (INSUrlSessionDelegate) new NSUrlSessionHandlerDelegate (this), null);
  149. inflightRequests = new Dictionary<NSUrlSessionTask, InflightData> ();
  150. }
  151. #if !MONOMAC && !MONOTOUCH_WATCH
  152. void AddNotification ()
  153. {
  154. if (!isBackgroundSession && notificationToken == null)
  155. notificationToken = NSNotificationCenter.DefaultCenter.AddObserver (UIApplication.WillResignActiveNotification, BackgroundNotificationCb);
  156. }
  157. void RemoveNotification ()
  158. {
  159. if (notificationToken != null) {
  160. NSNotificationCenter.DefaultCenter.RemoveObserver (notificationToken);
  161. notificationToken = null;
  162. }
  163. }
  164. void BackgroundNotificationCb (NSNotification obj)
  165. {
  166. // we do not need to call the lock, we call cancel on the source, that will trigger all the needed code to
  167. // clean the resources and such
  168. foreach (var r in inflightRequests.Values) {
  169. r.CompletionSource.TrySetCanceled ();
  170. }
  171. }
  172. #endif
  173. public long MaxInputInMemory { get; set; } = long.MaxValue;
  174. void RemoveInflightData (NSUrlSessionTask task, bool cancel = true)
  175. {
  176. lock (inflightRequestsLock) {
  177. inflightRequests.Remove (task);
  178. #if !MONOMAC && !MONOTOUCH_WATCH
  179. // do we need to be notified? If we have not inflightData, we do not
  180. if (inflightRequests.Count == 0)
  181. RemoveNotification ();
  182. #endif
  183. }
  184. if (cancel)
  185. task?.Cancel ();
  186. task?.Dispose ();
  187. }
  188. protected override void Dispose (bool disposing)
  189. {
  190. #if !MONOMAC && !MONOTOUCH_WATCH
  191. // remove the notification if present, method checks against null
  192. RemoveNotification ();
  193. #endif
  194. lock (inflightRequestsLock) {
  195. foreach (var pair in inflightRequests) {
  196. pair.Key?.Cancel ();
  197. pair.Key?.Dispose ();
  198. }
  199. inflightRequests.Clear ();
  200. }
  201. base.Dispose (disposing);
  202. }
  203. bool disableCaching;
  204. public bool DisableCaching {
  205. get {
  206. return disableCaching;
  207. }
  208. set {
  209. EnsureModifiability ();
  210. disableCaching = value;
  211. }
  212. }
  213. string GetHeaderSeparator (string name)
  214. {
  215. string value;
  216. if (!headerSeparators.TryGetValue (name, out value))
  217. value = ",";
  218. return value;
  219. }
  220. async Task<NSUrlRequest> CreateRequest (HttpRequestMessage request)
  221. {
  222. var stream = Stream.Null;
  223. var headers = request.Headers as IEnumerable<KeyValuePair<string, IEnumerable<string>>>;
  224. if (request.Content != null) {
  225. stream = await request.Content.ReadAsStreamAsync ().ConfigureAwait (false);
  226. headers = System.Linq.Enumerable.ToArray(headers.Union (request.Content.Headers));
  227. }
  228. var nsrequest = new NSMutableUrlRequest {
  229. AllowsCellularAccess = true,
  230. CachePolicy = DisableCaching ? NSUrlRequestCachePolicy.ReloadIgnoringCacheData : NSUrlRequestCachePolicy.UseProtocolCachePolicy,
  231. HttpMethod = request.Method.ToString ().ToUpperInvariant (),
  232. Url = NSUrl.FromString (request.RequestUri.AbsoluteUri),
  233. Headers = headers.Aggregate (new NSMutableDictionary (), (acc, x) => {
  234. acc.Add (new NSString (x.Key), new NSString (string.Join (GetHeaderSeparator (x.Key), x.Value)));
  235. return acc;
  236. })
  237. };
  238. if (stream != Stream.Null) {
  239. // HttpContent.TryComputeLength is `protected internal` :-( but it's indirectly called by headers
  240. var length = request.Content.Headers.ContentLength;
  241. if (length.HasValue && (length <= MaxInputInMemory))
  242. nsrequest.Body = NSData.FromStream (stream);
  243. else
  244. nsrequest.BodyStream = new WrappedNSInputStream (stream);
  245. }
  246. return nsrequest;
  247. }
  248. #if SYSTEM_NET_HTTP || MONOMAC
  249. internal
  250. #endif
  251. protected override async Task<HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
  252. {
  253. Volatile.Write (ref sentRequest, true);
  254. var nsrequest = await CreateRequest (request).ConfigureAwait(false);
  255. var dataTask = session.CreateDataTask (nsrequest);
  256. var tcs = new TaskCompletionSource<HttpResponseMessage> ();
  257. lock (inflightRequestsLock) {
  258. #if !MONOMAC && !MONOTOUCH_WATCH
  259. // Add the notification whenever needed
  260. AddNotification ();
  261. #endif
  262. inflightRequests.Add (dataTask, new InflightData {
  263. RequestUrl = request.RequestUri.AbsoluteUri,
  264. CompletionSource = tcs,
  265. CancellationToken = cancellationToken,
  266. Stream = new NSUrlSessionDataTaskStream (),
  267. Request = request
  268. });
  269. }
  270. if (dataTask.State == NSUrlSessionTaskState.Suspended)
  271. dataTask.Resume ();
  272. // as per documentation:
  273. // If this token is already in the canceled state, the
  274. // delegate will be run immediately and synchronously.
  275. // Any exception the delegate generates will be
  276. // propagated out of this method call.
  277. //
  278. // The execution of the register ensures that if we
  279. // receive a already cancelled token or it is cancelled
  280. // just before this call, we will cancel the task.
  281. // Other approaches are harder, since querying the state
  282. // of the token does not guarantee that in the next
  283. // execution a threads cancels it.
  284. cancellationToken.Register (() => {
  285. RemoveInflightData (dataTask);
  286. tcs.TrySetCanceled ();
  287. });
  288. return await tcs.Task.ConfigureAwait (false);
  289. }
  290. #if MONOMAC
  291. // Needed since we strip during linking since we're inside a product assembly.
  292. [Preserve (AllMembers = true)]
  293. #endif
  294. partial class NSUrlSessionHandlerDelegate : NSUrlSessionDataDelegate
  295. {
  296. readonly NSUrlSessionHandler sessionHandler;
  297. public NSUrlSessionHandlerDelegate (NSUrlSessionHandler handler)
  298. {
  299. sessionHandler = handler;
  300. }
  301. InflightData GetInflightData (NSUrlSessionTask task)
  302. {
  303. var inflight = default (InflightData);
  304. lock (sessionHandler.inflightRequestsLock)
  305. if (sessionHandler.inflightRequests.TryGetValue (task, out inflight)) {
  306. // ensure that we did not cancel the request, if we did, do cancel the task
  307. if (inflight.CancellationToken.IsCancellationRequested)
  308. task?.Cancel ();
  309. return inflight;
  310. }
  311. // 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
  312. task?.Cancel ();
  313. return null;
  314. }
  315. public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action<NSUrlSessionResponseDisposition> completionHandler)
  316. {
  317. var inflight = GetInflightData (dataTask);
  318. if (inflight == null)
  319. return;
  320. try {
  321. var urlResponse = (NSHttpUrlResponse)response;
  322. var status = (int)urlResponse.StatusCode;
  323. var content = new NSUrlSessionDataTaskStreamContent (inflight.Stream, () => {
  324. if (!inflight.Completed) {
  325. dataTask.Cancel ();
  326. }
  327. inflight.Disposed = true;
  328. inflight.Stream.TrySetException (new ObjectDisposedException ("The content stream was disposed."));
  329. sessionHandler.RemoveInflightData (dataTask);
  330. }, inflight.CancellationToken);
  331. // NB: The double cast is because of a Xamarin compiler bug
  332. var httpResponse = new HttpResponseMessage ((HttpStatusCode)status) {
  333. Content = content,
  334. RequestMessage = inflight.Request
  335. };
  336. httpResponse.RequestMessage.RequestUri = new Uri (urlResponse.Url.AbsoluteString);
  337. foreach (var v in urlResponse.AllHeaderFields) {
  338. // NB: Cocoa trolling us so hard by giving us back dummy dictionary entries
  339. if (v.Key == null || v.Value == null) continue;
  340. // NSUrlSession tries to be smart with cookies, we will not use the raw value but the ones provided by the cookie storage
  341. if (v.Key.ToString () == SetCookie) continue;
  342. httpResponse.Headers.TryAddWithoutValidation (v.Key.ToString (), v.Value.ToString ());
  343. httpResponse.Content.Headers.TryAddWithoutValidation (v.Key.ToString (), v.Value.ToString ());
  344. }
  345. var cookies = session.Configuration.HttpCookieStorage.CookiesForUrl (response.Url);
  346. for (var index = 0; index < cookies.Length; index++) {
  347. httpResponse.Headers.TryAddWithoutValidation (SetCookie, cookies [index].GetHeaderValue ());
  348. }
  349. inflight.Response = httpResponse;
  350. // We don't want to send the response back to the task just yet. Because we want to mimic .NET behavior
  351. // as much as possible. When the response is sent back in .NET, the content stream is ready to read or the
  352. // request has completed, because of this we want to send back the response in DidReceiveData or DidCompleteWithError
  353. if (dataTask.State == NSUrlSessionTaskState.Suspended)
  354. dataTask.Resume ();
  355. } catch (Exception ex) {
  356. inflight.CompletionSource.TrySetException (ex);
  357. inflight.Stream.TrySetException (ex);
  358. sessionHandler.RemoveInflightData (dataTask);
  359. }
  360. completionHandler (NSUrlSessionResponseDisposition.Allow);
  361. }
  362. public override void DidReceiveData (NSUrlSession session, NSUrlSessionDataTask dataTask, NSData data)
  363. {
  364. var inflight = GetInflightData (dataTask);
  365. if (inflight == null)
  366. return;
  367. inflight.Stream.Add (data);
  368. SetResponse (inflight);
  369. }
  370. public override void DidCompleteWithError (NSUrlSession session, NSUrlSessionTask task, NSError error)
  371. {
  372. var inflight = GetInflightData (task);
  373. // this can happen if the HTTP request times out and it is removed as part of the cancellation process
  374. if (inflight != null) {
  375. // set the stream as finished
  376. inflight.Stream.TrySetReceivedAllData ();
  377. // send the error or send the response back
  378. if (error != null) {
  379. inflight.Errored = true;
  380. var exc = createExceptionForNSError (error);
  381. inflight.CompletionSource.TrySetException (exc);
  382. inflight.Stream.TrySetException (exc);
  383. } else {
  384. inflight.Completed = true;
  385. SetResponse (inflight);
  386. }
  387. sessionHandler.RemoveInflightData (task, cancel: false);
  388. }
  389. }
  390. void SetResponse (InflightData inflight)
  391. {
  392. lock (inflight.Lock) {
  393. if (inflight.ResponseSent)
  394. return;
  395. if (inflight.CompletionSource.Task.IsCompleted)
  396. return;
  397. var httpResponse = inflight.Response;
  398. inflight.ResponseSent = true;
  399. // EVIL HACK: having TrySetResult inline was blocking the request from completing
  400. Task.Run (() => inflight.CompletionSource.TrySetResult (httpResponse));
  401. }
  402. }
  403. public override void WillCacheResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSCachedUrlResponse proposedResponse, Action<NSCachedUrlResponse> completionHandler)
  404. {
  405. completionHandler (sessionHandler.DisableCaching ? null : proposedResponse);
  406. }
  407. public override void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action<NSUrlRequest> completionHandler)
  408. {
  409. completionHandler (sessionHandler.AllowAutoRedirect ? newRequest : null);
  410. }
  411. public override void DidReceiveChallenge (NSUrlSession session, NSUrlSessionTask task, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler)
  412. {
  413. var inflight = GetInflightData (task);
  414. if (inflight == null)
  415. return;
  416. // case for the basic auth failing up front. As per apple documentation:
  417. // 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
  418. // the addValue(_:forHTTPHeaderField:) or setValue(_:forHTTPHeaderField:) methods:
  419. // Authorization
  420. // Connection
  421. // Host
  422. // Proxy-Authenticate
  423. // Proxy-Authorization
  424. // WWW-Authenticate
  425. // 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,
  426. // 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
  427. // header, it means that we do not have the correct credentials, in any other case just do what it is expected.
  428. if (challenge.PreviousFailureCount == 0) {
  429. var authHeader = inflight.Request?.Headers?.Authorization;
  430. if (!(string.IsNullOrEmpty (authHeader?.Scheme) && string.IsNullOrEmpty (authHeader?.Parameter))) {
  431. completionHandler (NSUrlSessionAuthChallengeDisposition.RejectProtectionSpace, null);
  432. return;
  433. }
  434. }
  435. if (sessionHandler.Credentials != null && TryGetAuthenticationType (challenge.ProtectionSpace, out string authType)) {
  436. NetworkCredential credentialsToUse = null;
  437. if (authType != RejectProtectionSpaceAuthType) {
  438. var uri = inflight.Request.RequestUri;
  439. credentialsToUse = sessionHandler.Credentials.GetCredential (uri, authType);
  440. }
  441. if (credentialsToUse != null) {
  442. var credential = new NSUrlCredential (credentialsToUse.UserName, credentialsToUse.Password, NSUrlCredentialPersistence.ForSession);
  443. completionHandler (NSUrlSessionAuthChallengeDisposition.UseCredential, credential);
  444. } else {
  445. // Rejecting the challenge allows the next authentication method in the request to be delivered to
  446. // the DidReceiveChallenge method. Another authentication method may have credentials available.
  447. completionHandler (NSUrlSessionAuthChallengeDisposition.RejectProtectionSpace, null);
  448. }
  449. } else {
  450. completionHandler (NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, challenge.ProposedCredential);
  451. }
  452. }
  453. static readonly string RejectProtectionSpaceAuthType = "reject";
  454. static bool TryGetAuthenticationType (NSUrlProtectionSpace protectionSpace, out string authenticationType)
  455. {
  456. if (protectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodNTLM) {
  457. authenticationType = "NTLM";
  458. } else if (protectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodHTTPBasic) {
  459. authenticationType = "basic";
  460. } else if (protectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodNegotiate ||
  461. protectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodHTMLForm ||
  462. protectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodHTTPDigest) {
  463. // Want to reject this authentication type to allow the next authentication method in the request to
  464. // be used.
  465. authenticationType = RejectProtectionSpaceAuthType;
  466. } else {
  467. // ServerTrust, ClientCertificate or Default.
  468. authenticationType = null;
  469. return false;
  470. }
  471. return true;
  472. }
  473. }
  474. #if MONOMAC
  475. // Needed since we strip during linking since we're inside a product assembly.
  476. [Preserve (AllMembers = true)]
  477. #endif
  478. class InflightData
  479. {
  480. public readonly object Lock = new object ();
  481. public string RequestUrl { get; set; }
  482. public TaskCompletionSource<HttpResponseMessage> CompletionSource { get; set; }
  483. public CancellationToken CancellationToken { get; set; }
  484. public NSUrlSessionDataTaskStream Stream { get; set; }
  485. public HttpRequestMessage Request { get; set; }
  486. public HttpResponseMessage Response { get; set; }
  487. public bool ResponseSent { get; set; }
  488. public bool Errored { get; set; }
  489. public bool Disposed { get; set; }
  490. public bool Completed { get; set; }
  491. public bool Done { get { return Errored || Disposed || Completed || CancellationToken.IsCancellationRequested; } }
  492. }
  493. #if MONOMAC
  494. // Needed since we strip during linking since we're inside a product assembly.
  495. [Preserve (AllMembers = true)]
  496. #endif
  497. class NSUrlSessionDataTaskStreamContent : StreamContent
  498. {
  499. Action disposed;
  500. public NSUrlSessionDataTaskStreamContent (NSUrlSessionDataTaskStream source, Action onDisposed, CancellationToken token) : base (source, token)
  501. {
  502. disposed = onDisposed;
  503. }
  504. protected override void Dispose (bool disposing)
  505. {
  506. var action = Interlocked.Exchange (ref disposed, null);
  507. action?.Invoke ();
  508. base.Dispose (disposing);
  509. }
  510. }
  511. #if MONOMAC
  512. // Needed since we strip during linking since we're inside a product assembly.
  513. [Preserve (AllMembers = true)]
  514. #endif
  515. class NSUrlSessionDataTaskStream : Stream
  516. {
  517. readonly Queue<NSData> data;
  518. readonly object dataLock = new object ();
  519. long position;
  520. long length;
  521. bool receivedAllData;
  522. Exception exc;
  523. NSData current;
  524. Stream currentStream;
  525. public NSUrlSessionDataTaskStream ()
  526. {
  527. data = new Queue<NSData> ();
  528. }
  529. public void Add (NSData d)
  530. {
  531. lock (dataLock) {
  532. data.Enqueue (d);
  533. length += (int)d.Length;
  534. }
  535. }
  536. public void TrySetReceivedAllData ()
  537. {
  538. receivedAllData = true;
  539. }
  540. public void TrySetException (Exception e)
  541. {
  542. exc = e;
  543. TrySetReceivedAllData ();
  544. }
  545. void ThrowIfNeeded (CancellationToken cancellationToken)
  546. {
  547. if (exc != null)
  548. throw exc;
  549. cancellationToken.ThrowIfCancellationRequested ();
  550. }
  551. public override int Read (byte [] buffer, int offset, int count)
  552. {
  553. return ReadAsync (buffer, offset, count).Result;
  554. }
  555. public override async Task<int> ReadAsync (byte [] buffer, int offset, int count, CancellationToken cancellationToken)
  556. {
  557. // try to throw on enter
  558. ThrowIfNeeded (cancellationToken);
  559. while (current == null) {
  560. lock (dataLock) {
  561. if (data.Count == 0 && receivedAllData && position == length)
  562. return 0;
  563. if (data.Count > 0 && current == null) {
  564. current = data.Peek ();
  565. currentStream = current.AsStream ();
  566. break;
  567. }
  568. }
  569. await Task.Delay (50).ConfigureAwait (false);
  570. }
  571. // try to throw again before read
  572. ThrowIfNeeded (cancellationToken);
  573. var d = currentStream;
  574. var bufferCount = Math.Min (count, (int)(d.Length - d.Position));
  575. var bytesRead = await d.ReadAsync (buffer, offset, bufferCount, cancellationToken).ConfigureAwait (false);
  576. // add the bytes read from the pointer to the position
  577. position += bytesRead;
  578. // remove the current primary reference if the current position has reached the end of the bytes
  579. if (d.Position == d.Length) {
  580. lock (dataLock) {
  581. // this is the same object, it was done to make the cleanup
  582. data.Dequeue ();
  583. currentStream?.Dispose ();
  584. // We cannot use current?.Dispose. The reason is the following one:
  585. // In the DidReceiveResponse, if iOS realizes that a buffer can be reused,
  586. // because the data is the same, it will do so. Such a situation does happen
  587. // between requests, that is, request A and request B will get the same NSData
  588. // (buffer) in the delegate. In this case, we cannot dispose the NSData because
  589. // it might be that a different request received it and it is present in
  590. // its NSUrlSessionDataTaskStream stream. We can only trust the gc to do the job
  591. // which is better than copying the data over.
  592. current = null;
  593. currentStream = null;
  594. }
  595. }
  596. return bytesRead;
  597. }
  598. public override bool CanRead => true;
  599. public override bool CanSeek => false;
  600. public override bool CanWrite => false;
  601. public override bool CanTimeout => false;
  602. public override long Length => length;
  603. public override void SetLength (long value)
  604. {
  605. throw new InvalidOperationException ();
  606. }
  607. public override long Position {
  608. get { return position; }
  609. set { throw new InvalidOperationException (); }
  610. }
  611. public override long Seek (long offset, SeekOrigin origin)
  612. {
  613. throw new InvalidOperationException ();
  614. }
  615. public override void Flush ()
  616. {
  617. throw new InvalidOperationException ();
  618. }
  619. public override void Write (byte [] buffer, int offset, int count)
  620. {
  621. throw new InvalidOperationException ();
  622. }
  623. }
  624. #if MONOMAC
  625. // Needed since we strip during linking since we're inside a product assembly.
  626. [Preserve (AllMembers = true)]
  627. #endif
  628. class WrappedNSInputStream : NSInputStream
  629. {
  630. NSStreamStatus status;
  631. CFRunLoopSource source;
  632. readonly Stream stream;
  633. bool notifying;
  634. public WrappedNSInputStream (Stream inputStream)
  635. {
  636. status = NSStreamStatus.NotOpen;
  637. stream = inputStream;
  638. source = new CFRunLoopSource (Handle);
  639. }
  640. public override NSStreamStatus Status => status;
  641. public override void Open ()
  642. {
  643. status = NSStreamStatus.Open;
  644. Notify (CFStreamEventType.OpenCompleted);
  645. }
  646. public override void Close ()
  647. {
  648. status = NSStreamStatus.Closed;
  649. }
  650. public override nint Read (IntPtr buffer, nuint len)
  651. {
  652. var sourceBytes = new byte [len];
  653. var read = stream.Read (sourceBytes, 0, (int)len);
  654. Marshal.Copy (sourceBytes, 0, buffer, (int)len);
  655. if (notifying)
  656. return read;
  657. notifying = true;
  658. if (stream.CanSeek && stream.Position == stream.Length) {
  659. Notify (CFStreamEventType.EndEncountered);
  660. status = NSStreamStatus.AtEnd;
  661. }
  662. notifying = false;
  663. return read;
  664. }
  665. public override bool HasBytesAvailable ()
  666. {
  667. return true;
  668. }
  669. protected override bool GetBuffer (out IntPtr buffer, out nuint len)
  670. {
  671. // Just call the base implemention (which will return false)
  672. return base.GetBuffer (out buffer, out len);
  673. }
  674. // NSInvalidArgumentException Reason: *** -propertyForKey: only defined for abstract class. Define -[System_Net_Http_NSUrlSessionHandler_WrappedNSInputStream propertyForKey:]!
  675. protected override NSObject GetProperty (NSString key)
  676. {
  677. return null;
  678. }
  679. protected override bool SetProperty (NSObject property, NSString key)
  680. {
  681. return false;
  682. }
  683. protected override bool SetCFClientFlags (CFStreamEventType inFlags, IntPtr inCallback, IntPtr inContextPtr)
  684. {
  685. // Just call the base implementation, which knows how to handle everything.
  686. return base.SetCFClientFlags (inFlags, inCallback, inContextPtr);
  687. }
  688. public override void Schedule (NSRunLoop aRunLoop, string mode)
  689. {
  690. var cfRunLoop = aRunLoop.GetCFRunLoop ();
  691. var nsMode = new NSString (mode);
  692. cfRunLoop.AddSource (source, nsMode);
  693. if (notifying)
  694. return;
  695. notifying = true;
  696. Notify (CFStreamEventType.HasBytesAvailable);
  697. notifying = false;
  698. }
  699. public override void Unschedule (NSRunLoop aRunLoop, string mode)
  700. {
  701. var cfRunLoop = aRunLoop.GetCFRunLoop ();
  702. var nsMode = new NSString (mode);
  703. cfRunLoop.RemoveSource (source, nsMode);
  704. }
  705. protected override void Dispose (bool disposing)
  706. {
  707. stream?.Dispose ();
  708. }
  709. }
  710. }
  711. }