diff options
author | Alexander Köplinger <alex.koeplinger@outlook.com> | 2019-03-01 14:20:27 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-03-01 14:20:27 +0300 |
commit | 5bf74a995465dd54ceb4a2311c2d6041258e8fe0 (patch) | |
tree | 3eea528b285091006cd5dbf1fcc9da711a3fcd2d /mcs/class/System.Net.Http | |
parent | 6c58376102564bd5940efa8d34b162334f79b2bf (diff) |
[System.Net.Http] Move NSUrlSessionHandler from xamarin-macios to mono (#13229)
* [System.Net.Http] Move NSUrlSessionHandler from xamarin-macios to mono
This allows us to remove the need for a special custom System.Net.Http.dll
build in the xamarin-macios repository.
Part of #11397
* Bump API snapshot submodule
* [csproj] Update project files
Diffstat (limited to 'mcs/class/System.Net.Http')
8 files changed, 1033 insertions, 3 deletions
diff --git a/mcs/class/System.Net.Http/Makefile b/mcs/class/System.Net.Http/Makefile index 5c4fb101334..bb225ced69b 100644 --- a/mcs/class/System.Net.Http/Makefile +++ b/mcs/class/System.Net.Http/Makefile @@ -11,6 +11,22 @@ ifeq (monodroid,$(PROFILE)) LIB_MCS_FLAGS += -d:XAMARIN_MODERN endif +ifneq (,$(filter monotouch monotouch_tv monotouch_watch,$(PROFILE))) +LIB_MCS_FLAGS += -D:XAMCORE_2_0 -D:XAMARIN_MODERN -D:SYSTEM_NET_HTTP -D:UNIFIED -D:__UNIFIED__ +endif + +ifeq (monotouch,$(PROFILE)) +API_BIN_REFS := Xamarin.iOS +endif + +ifeq (monotouch_tv,$(PROFILE)) +API_BIN_REFS := Xamarin.TVOS +endif + +ifeq (monotouch_watch,$(PROFILE)) +API_BIN_REFS := Xamarin.WatchOS +endif + TEST_LIB_REFS = System System.Core TEST_MCS_FLAGS = diff --git a/mcs/class/System.Net.Http/NSUrlSessionHandler.cs b/mcs/class/System.Net.Http/NSUrlSessionHandler.cs new file mode 100644 index 00000000000..e48129c0877 --- /dev/null +++ b/mcs/class/System.Net.Http/NSUrlSessionHandler.cs @@ -0,0 +1,859 @@ +// +// NSUrlSessionHandler.cs: +// +// Authors: +// Paul Betts <paul@paulbetts.org> +// Nick Berardi <nick@nickberardi.com> +// +// 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 (); + } + } + } +} diff --git a/mcs/class/System.Net.Http/RuntimeOptions.cs b/mcs/class/System.Net.Http/RuntimeOptions.cs new file mode 100644 index 00000000000..6e4c152ca0e --- /dev/null +++ b/mcs/class/System.Net.Http/RuntimeOptions.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using System.Text; +using System.Net.Http; + +#if XAMCORE_2_0 +using Foundation; +using ObjCRuntime; +#elif MONOMAC +using MonoMac.Foundation; +using MonoMac.ObjCRuntime; +#else +#error Unknown platform +#endif + +namespace System.Net.Http { + class RuntimeOptions + { + const string HttpClientHandlerValue = "HttpClientHandler"; + const string CFNetworkHandlerValue = "CFNetworkHandler"; + const string NSUrlSessionHandlerValue = "NSUrlSessionHandler"; + + const string DefaultTlsProviderValue = "default"; + const string LegacyTlsProviderValue = "legacy"; + const string AppleTlsProviderValue = "appletls"; + + string http_message_handler; + + + internal static RuntimeOptions Read () + { + // for iOS NSBundle.ResourcePath returns the path to the root of the app bundle + // for macOS apps NSBundle.ResourcePath returns foo.app/Contents/Resources + // for macOS frameworks NSBundle.ResourcePath returns foo.app/Versions/Current/Resources + Class bundle_finder = new Class (typeof (NSObject.NSObject_Disposer)); + var resource_dir = NSBundle.FromClass (bundle_finder).ResourcePath; + var plist_path = GetFileName (resource_dir); + + if (!File.Exists (plist_path)) + return null; + + using (var plist = NSDictionary.FromFile (plist_path)) { + var options = new RuntimeOptions (); + options.http_message_handler = (NSString) plist ["HttpMessageHandler"]; + return options; + } + } + +#if MONOMAC + [Preserve] +#endif + internal static HttpMessageHandler GetHttpMessageHandler () + { + RuntimeOptions options = null; + + try { + options = RuntimeOptions.Read (); + } catch (FileNotFoundException){ + // this happens on the Mono SDKs since we don't have a real Xamarin.iOS.dll so we can't resolve NSObject + } + + if (options == null) { +#if MONOTOUCH_WATCH + return new NSUrlSessionHandler (); +#else + return new HttpClientHandler (); +#endif + } + + // all types will be present as this is executed only when the linker is not enabled + var handler_name = options.http_message_handler; + var t = Type.GetType (handler_name, false); + + HttpMessageHandler handler = null; + if (t != null) + handler = Activator.CreateInstance (t) as HttpMessageHandler; + if (handler != null) + return handler; +#if MONOTOUCH_WATCH + Console.WriteLine ("{0} is not a valid HttpMessageHandler, defaulting to NSUrlSessionHandler", handler_name); + return new NSUrlSessionHandler (); +#else + Console.WriteLine ("{0} is not a valid HttpMessageHandler, defaulting to System.Net.Http.HttpClientHandler", handler_name); + return new HttpClientHandler (); +#endif + } + + // Use either Create() or Read(). + RuntimeOptions () + { + } + + static string GetFileName (string resource_dir) + { + return Path.Combine (resource_dir, "runtime-options.plist"); + } + } +} diff --git a/mcs/class/System.Net.Http/System.Net.Http.csproj b/mcs/class/System.Net.Http/System.Net.Http.csproj index e7f4273df99..a09eb91983e 100644 --- a/mcs/class/System.Net.Http/System.Net.Http.csproj +++ b/mcs/class/System.Net.Http/System.Net.Http.csproj @@ -39,17 +39,17 @@ <PropertyGroup Condition=" '$(Platform)' == 'monotouch' ">
<OutputPath>./../../class/lib/monotouch</OutputPath>
<IntermediateOutputPath>./../../class/obj/$(AssemblyName)-monotouch</IntermediateOutputPath>
- <DefineConstants>NET_1_1;NET_2_0;NET_2_1;NET_3_5;NET_4_0;NET_4_5;MOBILE;MOBILE_LEGACY;MONO;MONOTOUCH;DISABLE_REMOTING;DISABLE_COM;FEATURE_INTERCEPTABLE_THREADPOOL_CALLBACK;FULL_AOT_RUNTIME;LEGACY_HTTPCLIENT</DefineConstants>
+ <DefineConstants>NET_1_1;NET_2_0;NET_2_1;NET_3_5;NET_4_0;NET_4_5;MOBILE;MOBILE_LEGACY;MONO;MONOTOUCH;DISABLE_REMOTING;DISABLE_COM;FEATURE_INTERCEPTABLE_THREADPOOL_CALLBACK;FULL_AOT_RUNTIME;XAMCORE_2_0;XAMARIN_MODERN;SYSTEM_NET_HTTP;UNIFIED;__UNIFIED__;LEGACY_HTTPCLIENT</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Platform)' == 'monotouch_watch' ">
<OutputPath>./../../class/lib/monotouch_watch</OutputPath>
<IntermediateOutputPath>./../../class/obj/$(AssemblyName)-monotouch_watch</IntermediateOutputPath>
- <DefineConstants>NET_1_1;NET_2_0;NET_2_1;NET_3_5;NET_4_0;NET_4_5;MOBILE;MOBILE_LEGACY;MONO;MONOTOUCH;DISABLE_REMOTING;DISABLE_COM;FEATURE_INTERCEPTABLE_THREADPOOL_CALLBACK;FULL_AOT_RUNTIME;FEATURE_NO_BSD_SOCKETS;MONOTOUCH_WATCH;LEGACY_HTTPCLIENT</DefineConstants>
+ <DefineConstants>NET_1_1;NET_2_0;NET_2_1;NET_3_5;NET_4_0;NET_4_5;MOBILE;MOBILE_LEGACY;MONO;MONOTOUCH;DISABLE_REMOTING;DISABLE_COM;FEATURE_INTERCEPTABLE_THREADPOOL_CALLBACK;FULL_AOT_RUNTIME;FEATURE_NO_BSD_SOCKETS;MONOTOUCH_WATCH;XAMCORE_2_0;XAMARIN_MODERN;SYSTEM_NET_HTTP;UNIFIED;__UNIFIED__;LEGACY_HTTPCLIENT</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Platform)' == 'monotouch_tv' ">
<OutputPath>./../../class/lib/monotouch_tv</OutputPath>
<IntermediateOutputPath>./../../class/obj/$(AssemblyName)-monotouch_tv</IntermediateOutputPath>
- <DefineConstants>NET_1_1;NET_2_0;NET_2_1;NET_3_5;NET_4_0;NET_4_5;MOBILE;MOBILE_LEGACY;MONO;MONOTOUCH;DISABLE_REMOTING;DISABLE_COM;FEATURE_INTERCEPTABLE_THREADPOOL_CALLBACK;FULL_AOT_RUNTIME;MONOTOUCH_TV;LEGACY_HTTPCLIENT</DefineConstants>
+ <DefineConstants>NET_1_1;NET_2_0;NET_2_1;NET_3_5;NET_4_0;NET_4_5;MOBILE;MOBILE_LEGACY;MONO;MONOTOUCH;DISABLE_REMOTING;DISABLE_COM;FEATURE_INTERCEPTABLE_THREADPOOL_CALLBACK;FULL_AOT_RUNTIME;MONOTOUCH_TV;XAMCORE_2_0;XAMARIN_MODERN;SYSTEM_NET_HTTP;UNIFIED;__UNIFIED__;LEGACY_HTTPCLIENT</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Platform)' == 'testing_aot_full_interp' ">
<OutputPath>./../../class/lib/testing_aot_full_interp</OutputPath>
@@ -845,6 +845,9 @@ <When Condition="'$(Platform)' == 'monotouch_watch'">
<ItemGroup>
<Compile Include="HeaderUtils.Legacy.cs" />
+ <Compile Include="HttpClientEx.cs" />
+ <Compile Include="NSUrlSessionHandler.cs" />
+ <Compile Include="RuntimeOptions.cs" />
<Compile Include="System.Net.Http.Headers\AuthenticationHeaderValue.cs" />
<Compile Include="System.Net.Http.Headers\CacheControlHeaderValue.cs" />
<Compile Include="System.Net.Http.Headers\CollectionExtensions.cs" />
@@ -903,11 +906,16 @@ <ItemGroup>
<Compile Include="..\..\..\external\corefx\src\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\CancellationHelper.cs" />
<Compile Include="..\..\..\external\corefx\src\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\ConnectHelper.cs" />
+ <Compile Include="CFContentStream.cs" />
+ <Compile Include="CFNetworkHandler.cs" />
<Compile Include="HeaderUtils.Legacy.cs" />
+ <Compile Include="HttpClientEx.cs" />
<Compile Include="HttpClientHandler.Legacy.cs" />
<Compile Include="HttpClientHandler.cs" />
<Compile Include="IMonoHttpClientHandler.cs" />
<Compile Include="MonoWebRequestHandler.cs" />
+ <Compile Include="NSUrlSessionHandler.cs" />
+ <Compile Include="RuntimeOptions.cs" />
<Compile Include="System.Net.Http.Headers\AuthenticationHeaderValue.cs" />
<Compile Include="System.Net.Http.Headers\CacheControlHeaderValue.cs" />
<Compile Include="System.Net.Http.Headers\CollectionExtensions.cs" />
@@ -965,11 +973,16 @@ <ItemGroup>
<Compile Include="..\..\..\external\corefx\src\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\CancellationHelper.cs" />
<Compile Include="..\..\..\external\corefx\src\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\ConnectHelper.cs" />
+ <Compile Include="CFContentStream.cs" />
+ <Compile Include="CFNetworkHandler.cs" />
<Compile Include="HeaderUtils.Legacy.cs" />
+ <Compile Include="HttpClientEx.cs" />
<Compile Include="HttpClientHandler.Legacy.cs" />
<Compile Include="HttpClientHandler.cs" />
<Compile Include="IMonoHttpClientHandler.cs" />
<Compile Include="MonoWebRequestHandler.cs" />
+ <Compile Include="NSUrlSessionHandler.cs" />
+ <Compile Include="RuntimeOptions.cs" />
<Compile Include="System.Net.Http.Headers\AuthenticationHeaderValue.cs" />
<Compile Include="System.Net.Http.Headers\CacheControlHeaderValue.cs" />
<Compile Include="System.Net.Http.Headers\CollectionExtensions.cs" />
@@ -1094,6 +1107,27 @@ <ProjectReference Include="../System/System.csproj" />
<ProjectReference Include="../corlib/corlib.csproj" />
</ItemGroup>
+ <ItemGroup Condition=" '$(Platform)' == 'monotouch' ">
+ <Reference Include="./../../../external/binary-reference-assemblies/build/monotouch/Xamarin.iOS.dll">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>./../../../external/binary-reference-assemblies/build/monotouch/Xamarin.iOS.dll</HintPath>
+ <Private>False</Private>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup Condition=" '$(Platform)' == 'monotouch_watch' ">
+ <Reference Include="./../../../external/binary-reference-assemblies/build/monotouch/Xamarin.WatchOS.dll">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>./../../../external/binary-reference-assemblies/build/monotouch/Xamarin.WatchOS.dll</HintPath>
+ <Private>False</Private>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup Condition=" '$(Platform)' == 'monotouch_tv' ">
+ <Reference Include="./../../../external/binary-reference-assemblies/build/monotouch/Xamarin.TVOS.dll">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>./../../../external/binary-reference-assemblies/build/monotouch/Xamarin.TVOS.dll</HintPath>
+ <Private>False</Private>
+ </Reference>
+ </ItemGroup>
<ItemGroup Condition=" '$(Platform)' == 'testing_aot_full_interp' ">
<Reference Include="nunitlite">
<HintPath>..\lib\testing_aot_full_interp\nunitlite.dll</HintPath>
diff --git a/mcs/class/System.Net.Http/Test/System.Net.Http/HttpClientTest.cs b/mcs/class/System.Net.Http/Test/System.Net.Http/HttpClientTest.cs index 68911ff8907..1b05575a6e6 100644 --- a/mcs/class/System.Net.Http/Test/System.Net.Http/HttpClientTest.cs +++ b/mcs/class/System.Net.Http/Test/System.Net.Http/HttpClientTest.cs @@ -178,6 +178,16 @@ namespace MonoTests.System.Net.Http const int WaitTimeout = 5000; [Test] + public void Ctor () + { + var client = new HttpClient (); + Assert.IsNull (client.BaseAddress, "#1"); + Assert.IsNotNull (client.DefaultRequestHeaders, "#2"); // TODO: full check + Assert.AreEqual (int.MaxValue, client.MaxResponseContentBufferSize, "#3"); + Assert.AreEqual (TimeSpan.FromSeconds (100), client.Timeout, "#4"); + } + + [Test] public void Ctor_Default () { var client = HttpClientTestHelpers.CreateHttpClient (); diff --git a/mcs/class/System.Net.Http/monotouch_System.Net.Http.dll.sources b/mcs/class/System.Net.Http/monotouch_System.Net.Http.dll.sources index 704cd9f69ab..64cdbb68748 100644 --- a/mcs/class/System.Net.Http/monotouch_System.Net.Http.dll.sources +++ b/mcs/class/System.Net.Http/monotouch_System.Net.Http.dll.sources @@ -1 +1,6 @@ #include legacy.sources +CFContentStream.cs +CFNetworkHandler.cs +HttpClientEx.cs +NSUrlSessionHandler.cs +RuntimeOptions.cs diff --git a/mcs/class/System.Net.Http/monotouch_tv_System.Net.Http.dll.sources b/mcs/class/System.Net.Http/monotouch_tv_System.Net.Http.dll.sources index 704cd9f69ab..64cdbb68748 100644 --- a/mcs/class/System.Net.Http/monotouch_tv_System.Net.Http.dll.sources +++ b/mcs/class/System.Net.Http/monotouch_tv_System.Net.Http.dll.sources @@ -1 +1,6 @@ #include legacy.sources +CFContentStream.cs +CFNetworkHandler.cs +HttpClientEx.cs +NSUrlSessionHandler.cs +RuntimeOptions.cs diff --git a/mcs/class/System.Net.Http/monotouch_watch_System.Net.Http.dll.sources b/mcs/class/System.Net.Http/monotouch_watch_System.Net.Http.dll.sources index cc55cb95b3d..c8cb6acafb1 100644 --- a/mcs/class/System.Net.Http/monotouch_watch_System.Net.Http.dll.sources +++ b/mcs/class/System.Net.Http/monotouch_watch_System.Net.Http.dll.sources @@ -1,2 +1,5 @@ #include legacy.sources System.Net.Http/HttpClientHandler.platformnotsupported.cs +HttpClientEx.cs +NSUrlSessionHandler.cs +RuntimeOptions.cs |