From 9d3f716c15ecd5ed8553b0ea4ee45069ad84c057 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Thu, 11 Aug 2022 12:23:18 -0400 Subject: [PATCH 01/12] [Foundation] Move the NSHttpCookieExtensions out of the handler file. --- src/Foundation/NSUrlSessionHandler.cs | 52 ------------- .../NSHttpCookieExtensions.cs | 74 +++++++++++++++++++ src/Makefile | 8 ++ 3 files changed, 82 insertions(+), 52 deletions(-) create mode 100644 src/Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler.cs index 1882da06544..4092e1cd683 100644 --- a/src/Foundation/NSUrlSessionHandler.cs +++ b/src/Foundation/NSUrlSessionHandler.cs @@ -62,58 +62,6 @@ namespace Foundation { #endif public delegate bool NSUrlSessionHandlerTrustOverrideForUrlCallback (NSUrlSessionHandler sender, string url, SecTrust trust); - // 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 is not 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 is not null) - AppendSegment (header, NSHttpCookie.KeyComment.ToString (), cookie.Comment.ToString()); - - if (cookie.CommentUrl is not null) - AppendSegment (header, NSHttpCookie.KeyCommentUrl.ToString (), cookie.CommentUrl.ToString()); - - if (cookie.Properties.ContainsKey (NSHttpCookie.KeyDiscard)) - AppendSegment (header, NSHttpCookie.KeyDiscard.ToString (), null); - - if (cookie.ExpiresDate is not 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"; diff --git a/src/Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs b/src/Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs new file mode 100644 index 00000000000..fe91f5804a2 --- /dev/null +++ b/src/Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs @@ -0,0 +1,74 @@ +using System; +using System.Globalization; +using System.Text; +using System.Net.Http; +using System.Net.Http.Headers; + +using Foundation; + +#if !MONOMAC +using UIKit; +#endif + +#nullable enable + +// useful extensions for the class in order to set it in a header used by the NSUrlSessionHandler +// for cookie management. +#if !MONOMAC +namespace System.Net.Http { +#else +namespace Foundation { +#endif + + static class NSHttpCookieExtensions + { + static void AppendSegment (StringBuilder builder, string name, string? value) + { + if (builder.Length > 0) + builder.Append ("; "); + + builder.Append (name); + if (value is not 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 is not null) + AppendSegment (header, NSHttpCookie.KeyComment.ToString (), cookie.Comment.ToString()); + + if (cookie.CommentUrl is not null) + AppendSegment (header, NSHttpCookie.KeyCommentUrl.ToString (), cookie.CommentUrl.ToString()); + + if (cookie.Properties.ContainsKey (NSHttpCookie.KeyDiscard)) + AppendSegment (header, NSHttpCookie.KeyDiscard.ToString (), null); + + if (cookie.ExpiresDate is not 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 (); + } + } + +} diff --git a/src/Makefile b/src/Makefile index 478424ddae7..6e3cc9b772e 100644 --- a/src/Makefile +++ b/src/Makefile @@ -153,11 +153,13 @@ IOS_EXTRA_SOURCES = \ IOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ IOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ @@ -536,6 +538,7 @@ MAC_CFNETWORK_SOURCES = \ MAC_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ @@ -851,6 +854,7 @@ WATCHOS_EXTRA_CORE_SOURCES = \ WATCHOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ WATCHOS_CORE_SOURCES += \ $(WATCHOS_EXTRA_CORE_SOURCES) \ @@ -1143,12 +1147,14 @@ TVOS_CORE_SOURCES += \ TVOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ TVOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ @@ -1362,12 +1368,14 @@ MACCATALYST_DOTNET_CORE_SOURCES += \ MACCATALYST_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ MACCATALYST_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ From ab75335db80a60d6e456ebd1344f1cda2cf25787 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Thu, 11 Aug 2022 12:42:32 -0400 Subject: [PATCH 02/12] [Foundation] Move the MonoStreamContent out of the handler file. --- .../NSUrlSessionHandler/MonoStreamContent.cs | 111 ++++++++++++++++++ src/Makefile | 8 ++ 2 files changed, 119 insertions(+) create mode 100644 src/Foundation/NSUrlSessionHandler/MonoStreamContent.cs diff --git a/src/Foundation/NSUrlSessionHandler/MonoStreamContent.cs b/src/Foundation/NSUrlSessionHandler/MonoStreamContent.cs new file mode 100644 index 00000000000..e4dc84f6999 --- /dev/null +++ b/src/Foundation/NSUrlSessionHandler/MonoStreamContent.cs @@ -0,0 +1,111 @@ +// +// Copied from https://github.com/mono/mono/blob/2019-02/mcs/class/System.Net.Http/System.Net.Http/StreamContent.cs. +// +// This is not a perfect solution, but the most robust and risk-free approach. +// +// The implementation depends on Mono-specific behavior, which makes SerializeToStreamAsync() cancellable. +// Unfortunately, the CoreFX implementation of HttpClient does not support this. +// +// By copying Mono's old implementation here, we ensure that we're compatible with both HttpClient implementations, +// so when we eventually adopt the CoreFX version in all of Mono's profiles, we don't regress here. +// +using System; +using System.Net; +using System.Net.Http; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +#if !MONOMAC +namespace System.Net.Http { +#else +namespace Foundation { +#endif + + class MonoStreamContent : HttpContent + { + readonly Stream content; + readonly int bufferSize; + readonly CancellationToken cancellationToken; + readonly long startPosition; + bool contentCopied; + + public MonoStreamContent (Stream content) + : this (content, 16 * 1024) + { + } + + public MonoStreamContent (Stream content, int bufferSize) + { + if (content is null) + ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (content)); + + if (bufferSize <= 0) + ObjCRuntime.ThrowHelper.ThrowArgumentOutOfRangeException (nameof (bufferSize), bufferSize, "Buffer size must be >0"); + + this.content = content; + this.bufferSize = bufferSize; + + if (content.CanSeek) { + startPosition = content.Position; + } + } + + // + // Workarounds for poor .NET API + // Instead of having SerializeToStreamAsync with CancellationToken as public API. Only LoadIntoBufferAsync + // called internally from the send worker can be cancelled and user cannot see/do it + // + internal MonoStreamContent (Stream content, CancellationToken cancellationToken) + : this (content) + { + // We don't own the token so don't worry about disposing it + this.cancellationToken = cancellationToken; + } + + protected override Task CreateContentReadStreamAsync () + { + return Task.FromResult (content); + } + + protected override void Dispose (bool disposing) + { + if (disposing) { + content.Dispose (); + } + + base.Dispose (disposing); + } + + protected override Task SerializeToStreamAsync (Stream stream, TransportContext? context) + { + if (contentCopied) { + if (!content.CanSeek) { + throw new InvalidOperationException ("The stream was already consumed. It cannot be read again."); + } + + content.Seek (startPosition, SeekOrigin.Begin); + } else { + contentCopied = true; + } + + return content.CopyToAsync (stream, bufferSize, cancellationToken); + } + +#if !NET + internal +#endif + protected override bool TryComputeLength (out long length) + { + if (!content.CanSeek) { + length = 0; + return false; + } + length = content.Length - startPosition; + return true; + } + } + +} diff --git a/src/Makefile b/src/Makefile index 6e3cc9b772e..d3be850999d 100644 --- a/src/Makefile +++ b/src/Makefile @@ -154,12 +154,14 @@ IOS_EXTRA_SOURCES = \ IOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ IOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ @@ -539,6 +541,7 @@ MAC_CFNETWORK_SOURCES = \ MAC_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ @@ -855,6 +858,7 @@ WATCHOS_EXTRA_CORE_SOURCES = \ WATCHOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ WATCHOS_CORE_SOURCES += \ $(WATCHOS_EXTRA_CORE_SOURCES) \ @@ -1148,6 +1152,7 @@ TVOS_CORE_SOURCES += \ TVOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ @@ -1155,6 +1160,7 @@ TVOS_DOTNET_HTTP_SOURCES = \ TVOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ @@ -1369,6 +1375,7 @@ MACCATALYST_DOTNET_CORE_SOURCES += \ MACCATALYST_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ @@ -1376,6 +1383,7 @@ MACCATALYST_DOTNET_HTTP_SOURCES = \ MACCATALYST_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ From 96e268d61481e1b0f7b68673b74208c4184b1131 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Thu, 11 Aug 2022 13:00:23 -0400 Subject: [PATCH 03/12] [Foundation] Move the WrappedNSInputStream out of the handler file. --- src/Foundation/NSUrlSessionHandler.cs | 212 +----------------- .../WrappedNSInputStream.cs | 130 +++++++++++ src/Makefile | 8 + 3 files changed, 139 insertions(+), 211 deletions(-) create mode 100644 src/Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler.cs index 4092e1cd683..e2becb97080 100644 --- a/src/Foundation/NSUrlSessionHandler.cs +++ b/src/Foundation/NSUrlSessionHandler.cs @@ -27,7 +27,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Net; using System.Net.Http; @@ -1004,101 +1003,6 @@ protected override void Dispose (bool disposing) } } - // - // Copied from https://github.com/mono/mono/blob/2019-02/mcs/class/System.Net.Http/System.Net.Http/StreamContent.cs. - // - // This is not a perfect solution, but the most robust and risk-free approach. - // - // The implementation depends on Mono-specific behavior, which makes SerializeToStreamAsync() cancellable. - // Unfortunately, the CoreFX implementation of HttpClient does not support this. - // - // By copying Mono's old implementation here, we ensure that we're compatible with both HttpClient implementations, - // so when we eventually adopt the CoreFX version in all of Mono's profiles, we don't regress here. - // - class MonoStreamContent : HttpContent - { - readonly Stream content; - readonly int bufferSize; - readonly CancellationToken cancellationToken; - readonly long startPosition; - bool contentCopied; - - public MonoStreamContent (Stream content) - : this (content, 16 * 1024) - { - } - - public MonoStreamContent (Stream content, int bufferSize) - { - if (content is null) - ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (content)); - - if (bufferSize <= 0) - ObjCRuntime.ThrowHelper.ThrowArgumentOutOfRangeException (nameof (bufferSize), bufferSize, "Buffer size must be >0"); - - this.content = content; - this.bufferSize = bufferSize; - - if (content.CanSeek) { - startPosition = content.Position; - } - } - - // - // Workarounds for poor .NET API - // Instead of having SerializeToStreamAsync with CancellationToken as public API. Only LoadIntoBufferAsync - // called internally from the send worker can be cancelled and user cannot see/do it - // - internal MonoStreamContent (Stream content, CancellationToken cancellationToken) - : this (content) - { - // We don't own the token so don't worry about disposing it - this.cancellationToken = cancellationToken; - } - - protected override Task CreateContentReadStreamAsync () - { - return Task.FromResult (content); - } - - protected override void Dispose (bool disposing) - { - if (disposing) { - content.Dispose (); - } - - base.Dispose (disposing); - } - - protected override Task SerializeToStreamAsync (Stream stream, TransportContext? context) - { - if (contentCopied) { - if (!content.CanSeek) { - throw new InvalidOperationException ("The stream was already consumed. It cannot be read again."); - } - - content.Seek (startPosition, SeekOrigin.Begin); - } else { - contentCopied = true; - } - - return content.CopyToAsync (stream, bufferSize, cancellationToken); - } - -#if !NET - internal -#endif - protected override bool TryComputeLength (out long length) - { - if (!content.CanSeek) { - length = 0; - return false; - } - length = content.Length - startPosition; - return true; - } - } - class NSUrlSessionDataTaskStream : Stream { readonly Queue data; @@ -1243,120 +1147,6 @@ public override void Write (byte [] buffer, int offset, int count) throw new InvalidOperationException (); } } - - 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, false); - } - - 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); - } - -#if NET - public override void Schedule (NSRunLoop aRunLoop, NSString nsMode) -#else - public override void Schedule (NSRunLoop aRunLoop, string mode) -#endif - { - var cfRunLoop = aRunLoop.GetCFRunLoop (); -#if !NET - var nsMode = new NSString (mode); -#endif - - cfRunLoop.AddSource (source, nsMode); - - if (notifying) - return; - - notifying = true; - Notify (CFStreamEventType.HasBytesAvailable); - notifying = false; - } - -#if NET - public override void Unschedule (NSRunLoop aRunLoop, NSString nsMode) -#else - public override void Unschedule (NSRunLoop aRunLoop, string mode) -#endif - { - var cfRunLoop = aRunLoop.GetCFRunLoop (); -#if !NET - var nsMode = new NSString (mode); -#endif - - cfRunLoop.RemoveSource (source, nsMode); - } - - protected override void Dispose (bool disposing) - { - stream?.Dispose (); - } - } } + } diff --git a/src/Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs b/src/Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs new file mode 100644 index 00000000000..2724374a974 --- /dev/null +++ b/src/Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +using CoreFoundation; +using Foundation; + +#nullable enable + +#if !MONOMAC +namespace System.Net.Http { +#else +namespace Foundation { +#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, false); + } + + 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); + } + +#if NET + public override void Schedule (NSRunLoop aRunLoop, NSString nsMode) +#else + public override void Schedule (NSRunLoop aRunLoop, string mode) +#endif + { + var cfRunLoop = aRunLoop.GetCFRunLoop (); +#if !NET + var nsMode = new NSString (mode); +#endif + + cfRunLoop.AddSource (source, nsMode); + + if (notifying) + return; + + notifying = true; + Notify (CFStreamEventType.HasBytesAvailable); + notifying = false; + } + +#if NET + public override void Unschedule (NSRunLoop aRunLoop, NSString nsMode) +#else + public override void Unschedule (NSRunLoop aRunLoop, string mode) +#endif + { + var cfRunLoop = aRunLoop.GetCFRunLoop (); +#if !NET + var nsMode = new NSString (mode); +#endif + + cfRunLoop.RemoveSource (source, nsMode); + } + + protected override void Dispose (bool disposing) + { + stream?.Dispose (); + } + } +} diff --git a/src/Makefile b/src/Makefile index d3be850999d..0fb72e66065 100644 --- a/src/Makefile +++ b/src/Makefile @@ -155,6 +155,7 @@ IOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ + Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ @@ -162,6 +163,7 @@ IOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ + Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ @@ -542,6 +544,7 @@ MAC_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ + Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ @@ -859,6 +862,7 @@ WATCHOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ + Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ WATCHOS_CORE_SOURCES += \ $(WATCHOS_EXTRA_CORE_SOURCES) \ @@ -1153,6 +1157,7 @@ TVOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ + Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ @@ -1161,6 +1166,7 @@ TVOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ + Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ @@ -1376,6 +1382,7 @@ MACCATALYST_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ + Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ @@ -1384,6 +1391,7 @@ MACCATALYST_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ + Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ System.Net.Http/CFNetworkHandler.cs \ $(SHARED_DESIGNER_CS) \ From b2ef8939163798d909f8c81c2ad74cb3c5c41200 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Thu, 11 Aug 2022 13:21:12 -0400 Subject: [PATCH 04/12] [Foundation] Move the NSUrlSessionDataTaskStream out of the handler file. --- .../NSUrlSessionDataTaskStream.cs | 161 ++++++++++++++++++ src/Makefile | 8 + 2 files changed, 169 insertions(+) create mode 100644 src/Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs diff --git a/src/Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs b/src/Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs new file mode 100644 index 00000000000..063298bf5e0 --- /dev/null +++ b/src/Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Foundation; + +#nullable enable + +#if !MONOMAC +namespace System.Net.Http { +#else +namespace Foundation { +#endif + + class NSUrlSessionDataTaskStream : Stream + { + readonly Queue data; + readonly object dataLock = new object (); + + long position; + long length; + + bool receivedAllData; + Exception? exc; + + NSData? current; + Stream? currentStream; + + public NSUrlSessionDataTaskStream () + { + data = new Queue (); + } + + 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 is not 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 ReadAsync (byte [] buffer, int offset, int count, CancellationToken cancellationToken) + { + // try to throw on enter + ThrowIfNeeded (cancellationToken); + + while (current is null) { + lock (dataLock) { + if (data.Count == 0 && receivedAllData && position == length) + return 0; + + if (data.Count > 0 && current is null) { + current = data.Peek (); + currentStream = current.AsStream (); + break; + } + } + + try { + await Task.Delay (50, cancellationToken).ConfigureAwait (false); + } catch (TaskCanceledException ex) { + // add a nicer exception for the user to catch, add the cancelation exception + // to have a decent stack + throw new TimeoutException ("The request timed out.", ex); + } + } + + // 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 (); + } + } +} diff --git a/src/Makefile b/src/Makefile index 0fb72e66065..43ce618ee57 100644 --- a/src/Makefile +++ b/src/Makefile @@ -154,6 +154,7 @@ IOS_EXTRA_SOURCES = \ IOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -162,6 +163,7 @@ IOS_DOTNET_HTTP_SOURCES = \ IOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -543,6 +545,7 @@ MAC_CFNETWORK_SOURCES = \ MAC_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -861,6 +864,7 @@ WATCHOS_EXTRA_CORE_SOURCES = \ WATCHOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ @@ -1156,6 +1160,7 @@ TVOS_CORE_SOURCES += \ TVOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -1165,6 +1170,7 @@ TVOS_DOTNET_HTTP_SOURCES = \ TVOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -1381,6 +1387,7 @@ MACCATALYST_DOTNET_CORE_SOURCES += \ MACCATALYST_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -1390,6 +1397,7 @@ MACCATALYST_DOTNET_HTTP_SOURCES = \ MACCATALYST_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ From 1ec1946f8252bb0e3c000fd879af6d9d8b16379d Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Thu, 11 Aug 2022 13:31:49 -0400 Subject: [PATCH 05/12] [Foundation] Move the NSUrlSessionDataTaskStreamContent out of the handler file. --- src/Foundation/NSUrlSessionHandler.cs | 146 ------------------ .../NSUrlSessionDataTaskStreamContent.cs | 29 ++++ src/Makefile | 8 + 3 files changed, 37 insertions(+), 146 deletions(-) create mode 100644 src/Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler.cs index e2becb97080..644ba38c2b7 100644 --- a/src/Foundation/NSUrlSessionHandler.cs +++ b/src/Foundation/NSUrlSessionHandler.cs @@ -1002,151 +1002,5 @@ protected override void Dispose (bool disposing) base.Dispose (disposing); } } - - class NSUrlSessionDataTaskStream : Stream - { - readonly Queue data; - readonly object dataLock = new object (); - - long position; - long length; - - bool receivedAllData; - Exception? exc; - - NSData? current; - Stream? currentStream; - - public NSUrlSessionDataTaskStream () - { - data = new Queue (); - } - - 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 is not 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 ReadAsync (byte [] buffer, int offset, int count, CancellationToken cancellationToken) - { - // try to throw on enter - ThrowIfNeeded (cancellationToken); - - while (current is null) { - lock (dataLock) { - if (data.Count == 0 && receivedAllData && position == length) - return 0; - - if (data.Count > 0 && current is null) { - current = data.Peek (); - currentStream = current.AsStream (); - break; - } - } - - try { - await Task.Delay (50, cancellationToken).ConfigureAwait (false); - } catch (TaskCanceledException ex) { - // add a nicer exception for the user to catch, add the cancelation exception - // to have a decent stack - throw new TimeoutException ("The request timed out.", ex); - } - } - - // 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 (); - } - } } - } diff --git a/src/Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs b/src/Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs new file mode 100644 index 00000000000..d1e093aa1f7 --- /dev/null +++ b/src/Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; + +#nullable enable + +#if !MONOMAC +namespace System.Net.Http { +#else +namespace Foundation { +#endif + + class NSUrlSessionDataTaskStreamContent : MonoStreamContent + { + 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); + } + } +} diff --git a/src/Makefile b/src/Makefile index 43ce618ee57..b0eed8c9867 100644 --- a/src/Makefile +++ b/src/Makefile @@ -155,6 +155,7 @@ IOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -164,6 +165,7 @@ IOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -546,6 +548,7 @@ MAC_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -865,6 +868,7 @@ WATCHOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ @@ -1161,6 +1165,7 @@ TVOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -1171,6 +1176,7 @@ TVOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -1388,6 +1394,7 @@ MACCATALYST_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -1398,6 +1405,7 @@ MACCATALYST_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ From e791d3fcf6eb6c1fafb2a9c11ea66a020e2dc45a Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Fri, 12 Aug 2022 10:42:53 -0400 Subject: [PATCH 06/12] [Foundation] Move the InflightData out of the handler file. --- src/Foundation/NSUrlSessionHandler.cs | 60 ------------------- .../NSUrlSessionHandler/InflightData.cs | 57 ++++++++++++++++++ src/Makefile | 8 +++ 3 files changed, 65 insertions(+), 60 deletions(-) create mode 100644 src/Foundation/NSUrlSessionHandler/InflightData.cs diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler.cs index 644ba38c2b7..06347c36e23 100644 --- a/src/Foundation/NSUrlSessionHandler.cs +++ b/src/Foundation/NSUrlSessionHandler.cs @@ -942,65 +942,5 @@ static bool TryGetAuthenticationType (NSUrlProtectionSpace protectionSpace, [Not return true; } } - - class InflightData : IDisposable - { - public readonly object Lock = new object (); - public string RequestUrl { get; set; } - - public TaskCompletionSource CompletionSource { get; } = new TaskCompletionSource (); - public CancellationToken CancellationToken { get; set; } - public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource (); - public NSUrlSessionDataTaskStream Stream { get; } = new NSUrlSessionDataTaskStream (); - public HttpRequestMessage Request { get; set; } - public HttpResponseMessage? Response { get; set; } - - public Exception? Exception { get; set; } - public bool ResponseSent { get; set; } - public bool Errored { get; set; } - public bool Disposed { get; set; } - public bool Completed { get; set; } - public bool Done { get { return Errored || Disposed || Completed || CancellationToken.IsCancellationRequested; } } - - public InflightData (string requestUrl, CancellationToken cancellationToken, HttpRequestMessage request) - { - RequestUrl = requestUrl; - CancellationToken = cancellationToken; - Request = request; - } - - public void Dispose() - { - Dispose (true); - GC.SuppressFinalize(this); - } - - // The bulk of the clean-up code is implemented in Dispose(bool) - protected virtual void Dispose (bool disposing) - { - if (disposing) { - CancellationTokenSource.Dispose (); - } - } - - } - - class NSUrlSessionDataTaskStreamContent : MonoStreamContent - { - 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); - } - } } } diff --git a/src/Foundation/NSUrlSessionHandler/InflightData.cs b/src/Foundation/NSUrlSessionHandler/InflightData.cs new file mode 100644 index 00000000000..049beb137bf --- /dev/null +++ b/src/Foundation/NSUrlSessionHandler/InflightData.cs @@ -0,0 +1,57 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable + +#if !MONOMAC +namespace System.Net.Http { +#else +namespace Foundation { +#endif + + class InflightData : IDisposable + { + public readonly object Lock = new object (); + public string RequestUrl { get; set; } + + public TaskCompletionSource CompletionSource { get; } = new TaskCompletionSource (); + public CancellationToken CancellationToken { get; set; } + public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource (); + public NSUrlSessionDataTaskStream Stream { get; } = new NSUrlSessionDataTaskStream (); + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage? Response { get; set; } + + public Exception? Exception { get; set; } + public bool ResponseSent { get; set; } + public bool Errored { get; set; } + public bool Disposed { get; set; } + public bool Completed { get; set; } + // CancellationToken.IsCancellationRequested + public bool Done { get { return Errored || Disposed || Completed; } } + + public InflightData (string requestUrl, CancellationToken cancellationToken, HttpRequestMessage request) + { + RequestUrl = requestUrl; + CancellationToken = cancellationToken; + Request = request; + } + + public void Dispose() + { + Dispose (true); + GC.SuppressFinalize(this); + } + + // The bulk of the clean-up code is implemented in Dispose(bool) + protected virtual void Dispose (bool disposing) + { + if (disposing) { + CancellationTokenSource.Dispose (); + } + } + + } + +} diff --git a/src/Makefile b/src/Makefile index b0eed8c9867..ecff05a3ba3 100644 --- a/src/Makefile +++ b/src/Makefile @@ -156,6 +156,7 @@ IOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -166,6 +167,7 @@ IOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -549,6 +551,7 @@ MAC_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -869,6 +872,7 @@ WATCHOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ @@ -1166,6 +1170,7 @@ TVOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -1177,6 +1182,7 @@ TVOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -1395,6 +1401,7 @@ MACCATALYST_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ @@ -1406,6 +1413,7 @@ MACCATALYST_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ System.Net.Http/CFContentStream.cs \ From 2d17436146eadc42e9276d78457447891afbcdf8 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Fri, 12 Aug 2022 11:16:53 -0400 Subject: [PATCH 07/12] [Foundation] Move the NSUrlSessionHandlerDelegate out of the handler file. --- src/Foundation/NSUrlSessionHandler.cs | 358 +---------------- .../NSUrlSessionHandlerDelegate.cs | 371 ++++++++++++++++++ src/Makefile | 8 + 3 files changed, 383 insertions(+), 354 deletions(-) create mode 100644 src/Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler.cs index 06347c36e23..532780dfb12 100644 --- a/src/Foundation/NSUrlSessionHandler.cs +++ b/src/Foundation/NSUrlSessionHandler.cs @@ -63,17 +63,16 @@ namespace Foundation { public partial class NSUrlSessionHandler : HttpMessageHandler { - private const string SetCookie = "Set-Cookie"; private const string Cookie = "Cookie"; - private CookieContainer? cookieContainer; + internal CookieContainer? cookieContainer; readonly Dictionary headerSeparators = new Dictionary { ["User-Agent"] = " ", ["Server"] = " " }; NSUrlSession session; - readonly Dictionary inflightRequests; - readonly object inflightRequestsLock = new object (); + internal readonly Dictionary inflightRequests; + internal readonly object inflightRequestsLock = new object (); readonly NSUrlSessionConfiguration.SessionConfigurationType sessionType; #if !MONOMAC && !__WATCHOS__ NSObject? notificationToken; // needed to make sure we do not hang if not using a background session @@ -163,7 +162,7 @@ void BackgroundNotificationCb (NSNotification obj) public long MaxInputInMemory { get; set; } = long.MaxValue; - void RemoveInflightData (NSUrlSessionTask task, bool cancel = true) + internal void RemoveInflightData (NSUrlSessionTask task, bool cancel = true) { lock (inflightRequestsLock) { if (inflightRequests.TryGetValue (task, out var data)) { @@ -345,34 +344,6 @@ internal void EnsureModifiability () "Properties can only be modified before sending the first request."); } - static Exception createExceptionForNSError(NSError error) - { - var innerException = new NSErrorException(error); - - // errors that exists in both share the same error code, so we can use a single switch/case - // this also ease watchOS integration as if does not expose CFNetwork but (I would not be - // surprised if it)could return some of it's error codes -#if __WATCHOS__ - if (error.Domain == NSError.NSUrlErrorDomain) { -#else - if ((error.Domain == NSError.NSUrlErrorDomain) || (error.Domain == NSError.CFNetworkErrorDomain)) { -#endif - // Apple docs: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Constants/index.html#//apple_ref/doc/constant_group/URL_Loading_System_Error_Codes - // .NET docs: http://msdn.microsoft.com/en-us/library/system.net.webexceptionstatus(v=vs.110).aspx - switch ((NSUrlError) (long) error.Code) { - case NSUrlError.Cancelled: - case NSUrlError.UserCancelledAuthentication: -#if !__WATCHOS__ - case (NSUrlError) NSNetServicesStatus.CancelledError: -#endif - // No more processing is required so just return. - return new OperationCanceledException(error.LocalizedDescription, innerException); - } - } - - return new HttpRequestException (error.LocalizedDescription, innerException); - } - string GetHeaderSeparator (string name) { if (!headerSeparators.TryGetValue (name, out var value)) @@ -621,326 +592,5 @@ public bool UseProxy { } #endif // NET - 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 we - // cancel the task it means that we are not interested in any of the delegate methods: - // - // DidReceiveResponse We might have received a response, but either the user cancelled or a - // timeout did, if that is the case, we do not care about the response. - // DidReceiveData Of buffer has a partial response ergo garbage and there is not real - // reason we would like to add more data. - // DidCompleteWithError - We are not changing a behaviour compared to the case in which - // we did not find the data. - if (inflight.CancellationToken.IsCancellationRequested) { - task?.Cancel (); - // return null so that we break out of any delegate method. - return null; - } - 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; - } - - void UpdateManagedCookieContainer (NSUrl url, NSHttpCookie[] cookies) - { - var uri = new Uri (url.AbsoluteString); - if (sessionHandler.cookieContainer is not null && cookies.Length > 0) - lock (sessionHandler.inflightRequestsLock) { // ensure we lock when writing to the collection - var cookiesContents = new string [cookies.Length]; - for (var index = 0; index < cookies.Length; index++) - cookiesContents [index] = cookies [index].GetHeaderValue (); - sessionHandler.cookieContainer.SetCookies (uri, string.Join (',', cookiesContents)); // as per docs: The contents of an HTTP set-cookie header as returned by a HTTP server, with Cookie instances delimited by commas. - } - } - - [Preserve (Conditional = true)] - public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action completionHandler) - { - var inflight = GetInflightData (dataTask); - - if (inflight is 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.CancellationTokenSource.Token); - - // 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 is null || v.Value is 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 ()); - } - - // it might be confusing that we are not using the managed CookieStore here, this is ONLY for those cookies that have been retrieved from - // the server via a Set-Cookie header, the managed container does not know a thing about this and apple is storing them in the native - // cookie container. Once we have the cookies from the response, we need to update the managed cookie container - if (session.Configuration.HttpCookieStorage is not null) { - var cookies = session.Configuration.HttpCookieStorage.CookiesForUrl (response.Url); - UpdateManagedCookieContainer (response.Url, cookies); - 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); - } - - [Preserve (Conditional = true)] - public override void DidReceiveData (NSUrlSession session, NSUrlSessionDataTask dataTask, NSData data) - { - var inflight = GetInflightData (dataTask); - - if (inflight is null) - return; - - inflight.Stream.Add (data); - SetResponse (inflight); - } - - [Preserve (Conditional = true)] - public override void DidCompleteWithError (NSUrlSession session, NSUrlSessionTask task, NSError? error) - { - var inflight = GetInflightData (task); - var serverError = task.Error; - - // this can happen if the HTTP request times out and it is removed as part of the cancellation process - if (inflight is not null) { - // set the stream as finished - inflight.Stream.TrySetReceivedAllData (); - - // send the error or send the response back - if (error is not null || serverError is not null) { - // got an error, cancel the stream operatios before we do anything - inflight.CancellationTokenSource.Cancel (); - inflight.Errored = true; - - var exc = inflight.Exception ?? createExceptionForNSError (error ?? serverError!); // client errors wont happen if we get server errors - 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.CancellationTokenSource.Token.IsCancellationRequested) - 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!)); - } - } - - [Preserve (Conditional = true)] - public override void WillCacheResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSCachedUrlResponse proposedResponse, Action completionHandler) - { - var inflight = GetInflightData (dataTask); - - if (inflight is null) - return; - // apple caches post request with a body, which should not happen. https://github.com/xamarin/maccore/issues/2571 - var disableCache = sessionHandler.DisableCaching || (inflight.Request.Method == HttpMethod.Post && inflight.Request.Content is not null); - completionHandler (disableCache ? null! : proposedResponse); - } - - [Preserve (Conditional = true)] - public override void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action completionHandler) - { - completionHandler (sessionHandler.AllowAutoRedirect ? newRequest : null!); - } - - [Preserve (Conditional = true)] - public override void DidReceiveChallenge (NSUrlSession session, NSUrlSessionTask task, NSUrlAuthenticationChallenge challenge, Action completionHandler) - { - var inflight = GetInflightData (task); - - if (inflight is null) - return; - - // ToCToU for the callback -#if !NET - var trustCallback = sessionHandler.TrustOverride; -#endif - var trustCallbackForUrl = sessionHandler.TrustOverrideForUrl; -#if NET - var hasCallBack = trustCallbackForUrl is not null; -#else - var hasCallBack = trustCallback is not null || trustCallbackForUrl is not null; -#endif - if (hasCallBack && challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodServerTrust) { -#if NET - // if the trust delegate allows to ignore the cert, do it. Since we are using nullables, if the delegate is not present, by default is false - var trustSec = (trustCallbackForUrl?.Invoke (sessionHandler, inflight.RequestUrl, challenge.ProtectionSpace.ServerSecTrust) ?? false); -#else - // if one of the delegates allows to ignore the cert, do it. We check first the one that takes the url because is more precisse, later the - // more general one. Since we are using nullables, if the delegate is not present, by default is false - var trustSec = (trustCallbackForUrl?.Invoke (sessionHandler, inflight.RequestUrl, challenge.ProtectionSpace.ServerSecTrust) ?? false) || - (trustCallback?.Invoke (sessionHandler, challenge.ProtectionSpace.ServerSecTrust) ?? false); -#endif - - if (trustSec) { - var credential = new NSUrlCredential (challenge.ProtectionSpace.ServerSecTrust); - completionHandler (NSUrlSessionAuthChallengeDisposition.UseCredential, credential); - } else { - // user callback rejected the certificate, we want to set the exception, else the user will - // see as if the request was cancelled. - lock (inflight.Lock) { - inflight.Exception = new HttpRequestException ("An error occurred while sending the request.", new WebException ("Error: TrustFailure")); - } - completionHandler (NSUrlSessionAuthChallengeDisposition.CancelAuthenticationChallenge, 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 is not null && TryGetAuthenticationType (challenge.ProtectionSpace, out var authType)) { - NetworkCredential? credentialsToUse = null; - if (authType != RejectProtectionSpaceAuthType) { - // interesting situation, when we use a credential that we created that is empty, we are not getting the RejectProtectionSpaceAuthType, - // nevertheless, we need to check is not the first challenge we will continue trusting the - // TryGetAuthenticationType method, but we will also check that the status response in not a 401 - // look like we do get an exception from the credentials db: - // TestiOSHttpClient[28769:26371051] CredStore - performQuery - Error copying matching creds. Error=-25300, query={ - // class = inet; - // "m_Limit" = "m_LimitAll"; - // ptcl = htps; - // "r_Attributes" = 1; - // sdmn = test; - // srvr = "jigsaw.w3.org"; - // sync = syna; - // } - // do remember that we ALWAYS get a challenge, so the failure count has to be 1 or more for this check, 1 would be the first time - var nsurlRespose = challenge.FailureResponse as NSHttpUrlResponse; - var responseIsUnauthorized = (nsurlRespose is null) ? false : nsurlRespose.StatusCode == (int) HttpStatusCode.Unauthorized && challenge.PreviousFailureCount > 0; - if (!responseIsUnauthorized) { - var uri = inflight.Request.RequestUri!; - credentialsToUse = sessionHandler.Credentials.GetCredential (uri, authType); - } - } - - if (credentialsToUse is not 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, [NotNullWhen (true)] 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; - } - } } } diff --git a/src/Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs b/src/Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs new file mode 100644 index 00000000000..d83847f585b --- /dev/null +++ b/src/Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs @@ -0,0 +1,371 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +using CoreFoundation; +using Foundation; +using Security; + +#nullable enable + +#if !MONOMAC +namespace System.Net.Http { +#else +namespace Foundation { +#endif + + partial class NSUrlSessionHandlerDelegate : NSUrlSessionDataDelegate + { + const string SetCookie = "Set-Cookie"; + readonly NSUrlSessionHandler sessionHandler; + + public NSUrlSessionHandlerDelegate (NSUrlSessionHandler handler) + { + sessionHandler = handler; + } + + static Exception createExceptionForNSError(NSError error) + { + var innerException = new NSErrorException(error); + + // errors that exists in both share the same error code, so we can use a single switch/case + // this also ease watchOS integration as if does not expose CFNetwork but (I would not be + // surprised if it)could return some of it's error codes +#if __WATCHOS__ + if (error.Domain == NSError.NSUrlErrorDomain) { +#else + if ((error.Domain == NSError.NSUrlErrorDomain) || (error.Domain == NSError.CFNetworkErrorDomain)) { +#endif + // Apple docs: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Constants/index.html#//apple_ref/doc/constant_group/URL_Loading_System_Error_Codes + // .NET docs: http://msdn.microsoft.com/en-us/library/system.net.webexceptionstatus(v=vs.110).aspx + switch ((NSUrlError) (long) error.Code) { + case NSUrlError.Cancelled: + case NSUrlError.UserCancelledAuthentication: +#if !__WATCHOS__ + case (NSUrlError) NSNetServicesStatus.CancelledError: +#endif + // No more processing is required so just return. + return new OperationCanceledException(error.LocalizedDescription, innerException); + } + } + + return new HttpRequestException (error.LocalizedDescription, innerException); + } + + + 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 we + // cancel the task it means that we are not interested in any of the delegate methods: + // + // DidReceiveResponse We might have received a response, but either the user cancelled or a + // timeout did, if that is the case, we do not care about the response. + // DidReceiveData Of buffer has a partial response ergo garbage and there is not real + // reason we would like to add more data. + // DidCompleteWithError - We are not changing a behaviour compared to the case in which + // we did not find the data. + if (inflight.CancellationToken.IsCancellationRequested) { + task?.Cancel (); + // return null so that we break out of any delegate method. + return null; + } + 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; + } + + void UpdateManagedCookieContainer (NSUrl url, NSHttpCookie[] cookies) + { + var uri = new Uri (url.AbsoluteString); + if (sessionHandler.cookieContainer is not null && cookies.Length > 0) + lock (sessionHandler.inflightRequestsLock) { // ensure we lock when writing to the collection + var cookiesContents = new string [cookies.Length]; + for (var index = 0; index < cookies.Length; index++) + cookiesContents [index] = cookies [index].GetHeaderValue (); + sessionHandler.cookieContainer.SetCookies (uri, string.Join (',', cookiesContents)); // as per docs: The contents of an HTTP set-cookie header as returned by a HTTP server, with Cookie instances delimited by commas. + } + } + + [Preserve (Conditional = true)] + public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action completionHandler) + { + var inflight = GetInflightData (dataTask); + + if (inflight is 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.CancellationTokenSource.Token); + + // 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 is null || v.Value is 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 ()); + } + + // it might be confusing that we are not using the managed CookieStore here, this is ONLY for those cookies that have been retrieved from + // the server via a Set-Cookie header, the managed container does not know a thing about this and apple is storing them in the native + // cookie container. Once we have the cookies from the response, we need to update the managed cookie container + if (session.Configuration.HttpCookieStorage is not null) { + var cookies = session.Configuration.HttpCookieStorage.CookiesForUrl (response.Url); + UpdateManagedCookieContainer (response.Url, cookies); + 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); + } + + [Preserve (Conditional = true)] + public override void DidReceiveData (NSUrlSession session, NSUrlSessionDataTask dataTask, NSData data) + { + var inflight = GetInflightData (dataTask); + + if (inflight is null) + return; + + inflight.Stream.Add (data); + SetResponse (inflight); + } + + [Preserve (Conditional = true)] + public override void DidCompleteWithError (NSUrlSession session, NSUrlSessionTask task, NSError? error) + { + var inflight = GetInflightData (task); + var serverError = task.Error; + + // this can happen if the HTTP request times out and it is removed as part of the cancellation process + if (inflight is not null) { + // set the stream as finished + inflight.Stream.TrySetReceivedAllData (); + + // send the error or send the response back + if (error is not null || serverError is not null) { + // got an error, cancel the stream operatios before we do anything + inflight.CancellationTokenSource.Cancel (); + inflight.Errored = true; + + var exc = inflight.Exception ?? createExceptionForNSError (error ?? serverError!); // client errors wont happen if we get server errors + 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.CancellationTokenSource.Token.IsCancellationRequested) + 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!)); + } + } + + [Preserve (Conditional = true)] + public override void WillCacheResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSCachedUrlResponse proposedResponse, Action completionHandler) + { + var inflight = GetInflightData (dataTask); + + if (inflight is null) + return; + // apple caches post request with a body, which should not happen. https://github.com/xamarin/maccore/issues/2571 + var disableCache = sessionHandler.DisableCaching || (inflight.Request.Method == HttpMethod.Post && inflight.Request.Content is not null); + completionHandler (disableCache ? null! : proposedResponse); + } + + [Preserve (Conditional = true)] + public override void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action completionHandler) + { + completionHandler (sessionHandler.AllowAutoRedirect ? newRequest : null!); + } + + [Preserve (Conditional = true)] + public override void DidReceiveChallenge (NSUrlSession session, NSUrlSessionTask task, NSUrlAuthenticationChallenge challenge, Action completionHandler) + { + var inflight = GetInflightData (task); + + if (inflight is null) + return; + + // ToCToU for the callback +#if !NET + var trustCallback = sessionHandler.TrustOverride; +#endif + var trustCallbackForUrl = sessionHandler.TrustOverrideForUrl; +#if NET + var hasCallBack = trustCallbackForUrl is not null; +#else + var hasCallBack = trustCallback is not null || trustCallbackForUrl is not null; +#endif + if (hasCallBack && challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodServerTrust) { +#if NET + // if the trust delegate allows to ignore the cert, do it. Since we are using nullables, if the delegate is not present, by default is false + var trustSec = (trustCallbackForUrl?.Invoke (sessionHandler, inflight.RequestUrl, challenge.ProtectionSpace.ServerSecTrust) ?? false); +#else + // if one of the delegates allows to ignore the cert, do it. We check first the one that takes the url because is more precisse, later the + // more general one. Since we are using nullables, if the delegate is not present, by default is false + var trustSec = (trustCallbackForUrl?.Invoke (sessionHandler, inflight.RequestUrl, challenge.ProtectionSpace.ServerSecTrust) ?? false) || + (trustCallback?.Invoke (sessionHandler, challenge.ProtectionSpace.ServerSecTrust) ?? false); +#endif + + if (trustSec) { + var credential = new NSUrlCredential (challenge.ProtectionSpace.ServerSecTrust); + completionHandler (NSUrlSessionAuthChallengeDisposition.UseCredential, credential); + } else { + // user callback rejected the certificate, we want to set the exception, else the user will + // see as if the request was cancelled. + lock (inflight.Lock) { + inflight.Exception = new HttpRequestException ("An error occurred while sending the request.", new WebException ("Error: TrustFailure")); + } + completionHandler (NSUrlSessionAuthChallengeDisposition.CancelAuthenticationChallenge, 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 is not null && TryGetAuthenticationType (challenge.ProtectionSpace, out var authType)) { + NetworkCredential? credentialsToUse = null; + if (authType != RejectProtectionSpaceAuthType) { + // interesting situation, when we use a credential that we created that is empty, we are not getting the RejectProtectionSpaceAuthType, + // nevertheless, we need to check is not the first challenge we will continue trusting the + // TryGetAuthenticationType method, but we will also check that the status response in not a 401 + // look like we do get an exception from the credentials db: + // TestiOSHttpClient[28769:26371051] CredStore - performQuery - Error copying matching creds. Error=-25300, query={ + // class = inet; + // "m_Limit" = "m_LimitAll"; + // ptcl = htps; + // "r_Attributes" = 1; + // sdmn = test; + // srvr = "jigsaw.w3.org"; + // sync = syna; + // } + // do remember that we ALWAYS get a challenge, so the failure count has to be 1 or more for this check, 1 would be the first time + var nsurlRespose = challenge.FailureResponse as NSHttpUrlResponse; + var responseIsUnauthorized = (nsurlRespose is null) ? false : nsurlRespose.StatusCode == (int) HttpStatusCode.Unauthorized && challenge.PreviousFailureCount > 0; + if (!responseIsUnauthorized) { + var uri = inflight.Request.RequestUri!; + credentialsToUse = sessionHandler.Credentials.GetCredential (uri, authType); + } + } + + if (credentialsToUse is not 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, [NotNullWhen (true)] 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; + } + } + +} diff --git a/src/Makefile b/src/Makefile index ecff05a3ba3..c0709e058fd 100644 --- a/src/Makefile +++ b/src/Makefile @@ -156,6 +156,7 @@ IOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs \ Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ @@ -167,6 +168,7 @@ IOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs \ Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ @@ -551,6 +553,7 @@ MAC_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs \ Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ @@ -872,6 +875,7 @@ WATCHOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs \ Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ @@ -1170,6 +1174,7 @@ TVOS_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs \ Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ @@ -1182,6 +1187,7 @@ TVOS_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs \ Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ @@ -1401,6 +1407,7 @@ MACCATALYST_DOTNET_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs \ Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ @@ -1413,6 +1420,7 @@ MACCATALYST_HTTP_SOURCES = \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs \ Foundation/NSUrlSessionHandler/InflightData.cs \ Foundation/NSUrlSessionHandler/MonoStreamContent.cs \ Foundation/NSUrlSessionHandler/WrappedNSInputStream.cs \ From d912334480bba3e5e34ec687b1402281629671ad Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Fri, 12 Aug 2022 15:50:26 -0400 Subject: [PATCH 08/12] [Foundation] Move the infligh data management to its own class. --- src/Foundation/NSUrlSessionHandler.cs | 106 ++++---------------------- 1 file changed, 16 insertions(+), 90 deletions(-) diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler.cs index 532780dfb12..41430a4e1b9 100644 --- a/src/Foundation/NSUrlSessionHandler.cs +++ b/src/Foundation/NSUrlSessionHandler.cs @@ -63,21 +63,16 @@ namespace Foundation { public partial class NSUrlSessionHandler : HttpMessageHandler { - private const string Cookie = "Cookie"; - internal CookieContainer? cookieContainer; + const string Cookie = "Cookie"; + NSUrlSession session; + readonly NSUrlSessionConfiguration.SessionConfigurationType sessionType; readonly Dictionary headerSeparators = new Dictionary { ["User-Agent"] = " ", ["Server"] = " " }; - NSUrlSession session; - internal readonly Dictionary inflightRequests; - internal readonly object inflightRequestsLock = new object (); - readonly NSUrlSessionConfiguration.SessionConfigurationType sessionType; -#if !MONOMAC && !__WATCHOS__ - NSObject? notificationToken; // needed to make sure we do not hang if not using a background session - readonly object notificationTokenLock = new object (); // need to make sure that threads do no step on each other with a dispose and a remove inflight data -#endif + internal CookieContainer? cookieContainer; + internal readonly NSUrlSessionHandlerInflightData inflightRequests; static NSUrlSessionConfiguration CreateConfig () { @@ -118,12 +113,15 @@ public NSUrlSessionHandler (NSUrlSessionConfiguration configuration) configuration.TLSMinimumSupportedProtocol = SslProtocol.Tls_1_3; session = NSUrlSession.FromConfiguration (configuration, (INSUrlSessionDelegate) new NSUrlSessionHandlerDelegate (this), null); - inflightRequests = new Dictionary (); + inflightRequests = new (this); } #if !MONOMAC && !__WATCHOS__ - void AddNotification () + NSObject? notificationToken; // needed to make sure we do not hang if not using a background session + readonly object notificationTokenLock = new object (); // need to make sure that threads do no step on each other with a dispose and a remove inflight data + + internal void AddNotification () { lock (notificationTokenLock) { if (!bypassBackgroundCheck && sessionType != NSUrlSessionConfiguration.SessionConfigurationType.Background && notificationToken is null) @@ -131,7 +129,7 @@ void AddNotification () } // lock } - void RemoveNotification () + internal void RemoveNotification () { NSObject? localNotificationToken; lock (notificationTokenLock) { @@ -142,62 +140,16 @@ void RemoveNotification () NSNotificationCenter.DefaultCenter.RemoveObserver (localNotificationToken); } - void BackgroundNotificationCb (NSNotification obj) - { - // the cancelation task of each of the sources will clean the different resources. Each removal is done - // inside a lock, but of course, the .Values collection will not like that because it is modified during the - // iteration. We split the operation in two, get all the diff cancelation sources, then try to cancel each of them - // which will do the correct lock dance. Note that we could be tempted to do a RemoveAll, that will yield the same - // runtime issue, this is dull but safe. - List> sources; - lock (inflightRequestsLock) { // just lock when we iterate - sources = new List > (inflightRequests.Count); - foreach (var r in inflightRequests.Values) { - sources.Add (r.CompletionSource); - } - } - sources.ForEach (source => { source.TrySetCanceled (); }); - } -#endif - - public long MaxInputInMemory { get; set; } = long.MaxValue; + void BackgroundNotificationCb (NSNotification _) => inflightRequests.CancelAll (); - internal void RemoveInflightData (NSUrlSessionTask task, bool cancel = true) - { - lock (inflightRequestsLock) { - if (inflightRequests.TryGetValue (task, out var data)) { - if (cancel) - data.CancellationTokenSource.Cancel (); - data.Dispose (); - inflightRequests.Remove (task); - } -#if !MONOMAC && !__WATCHOS__ - // 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 (); - } + public long MaxInputInMemory { get; set; } = long.MaxValue; protected override void Dispose (bool disposing) { - lock (inflightRequestsLock) { -#if !MONOMAC && !__WATCHOS__ - // remove the notification if present, method checks against null - RemoveNotification (); -#endif - foreach (var pair in inflightRequests) { - pair.Key?.Cancel (); - pair.Key?.Dispose (); - pair.Value?.Dispose (); - } - - inflightRequests.Clear (); + if (disposing) { + inflightRequests.Dispose (); } base.Dispose (disposing); } @@ -413,37 +365,11 @@ protected override async Task SendAsync (HttpRequestMessage var nsrequest = await CreateRequest (request).ConfigureAwait(false); var dataTask = session.CreateDataTask (nsrequest); - - var inflightData = new InflightData (request.RequestUri?.AbsoluteUri!, cancellationToken, request); - - lock (inflightRequestsLock) { -#if !MONOMAC && !__WATCHOS__ - // Add the notification whenever needed - AddNotification (); -#endif - inflightRequests.Add (dataTask, inflightData); - } + var inflightData = inflightRequests.Create (dataTask, request.RequestUri?.AbsoluteUri!, request, cancellationToken); 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); - inflightData.CompletionSource.TrySetCanceled (); - }); - return await inflightData.CompletionSource.Task.ConfigureAwait (false); } From 4210fa01bced24f9bd42dcc0f1ec27a5dcd9783a Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Fri, 12 Aug 2022 16:56:41 -0400 Subject: [PATCH 09/12] [Foundation] Separate the cancellation data from the inflight data. --- src/Foundation/NSUrlSessionHandler.cs | 4 +- .../NSUrlSessionHandler/InflightData.cs | 188 ++++++++++++++++-- .../NSUrlSessionHandlerDelegate.cs | 75 ++----- 3 files changed, 191 insertions(+), 76 deletions(-) diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler.cs index 41430a4e1b9..28ea81046cc 100644 --- a/src/Foundation/NSUrlSessionHandler.cs +++ b/src/Foundation/NSUrlSessionHandler.cs @@ -365,12 +365,12 @@ protected override async Task SendAsync (HttpRequestMessage var nsrequest = await CreateRequest (request).ConfigureAwait(false); var dataTask = session.CreateDataTask (nsrequest); - var inflightData = inflightRequests.Create (dataTask, request.RequestUri?.AbsoluteUri!, request, cancellationToken); + var (inflightData, cancellationData) = inflightRequests.Create (dataTask, request.RequestUri?.AbsoluteUri!, request, cancellationToken); if (dataTask.State == NSUrlSessionTaskState.Suspended) dataTask.Resume (); - return await inflightData.CompletionSource.Task.ConfigureAwait (false); + return await cancellationData.CompletionSource.Task.ConfigureAwait (false); } #if NET diff --git a/src/Foundation/NSUrlSessionHandler/InflightData.cs b/src/Foundation/NSUrlSessionHandler/InflightData.cs index 049beb137bf..cc88f429b8d 100644 --- a/src/Foundation/NSUrlSessionHandler/InflightData.cs +++ b/src/Foundation/NSUrlSessionHandler/InflightData.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Foundation; + #nullable enable #if !MONOMAC @@ -11,31 +14,149 @@ namespace System.Net.Http { namespace Foundation { #endif - class InflightData : IDisposable - { - public readonly object Lock = new object (); - public string RequestUrl { get; set; } + class NSUrlSessionHandlerInflightData : IDisposable { + readonly NSUrlSessionHandler sessionHandler; + readonly Dictionary inflightRequests = new (); - public TaskCompletionSource CompletionSource { get; } = new TaskCompletionSource (); - public CancellationToken CancellationToken { get; set; } - public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource (); - public NSUrlSessionDataTaskStream Stream { get; } = new NSUrlSessionDataTaskStream (); - public HttpRequestMessage Request { get; set; } - public HttpResponseMessage? Response { get; set; } + public object Lock { get; } = new object (); - public Exception? Exception { get; set; } - public bool ResponseSent { get; set; } - public bool Errored { get; set; } - public bool Disposed { get; set; } - public bool Completed { get; set; } - // CancellationToken.IsCancellationRequested - public bool Done { get { return Errored || Disposed || Completed; } } + public NSUrlSessionHandlerInflightData (NSUrlSessionHandler handler) { + sessionHandler = handler; + } + + public void CancelAll () { + // the cancelation task of each of the sources will clean the different resources. Each removal is done + // inside a lock, but of course, the .Values collection will not like that because it is modified during the + // iteration. We split the operation in two, get all the diff cancelation sources, then try to cancel each of them + // which will do the correct lock dance. Note that we could be tempted to do a RemoveAll, that will yield the same + // runtime issue, this is dull but safe. + List> sources; + lock (Lock) { // just lock when we iterate + sources = new (inflightRequests.Count); + foreach (var (_, cancellation) in inflightRequests.Values) { + sources.Add (cancellation.CompletionSource); + } + } + sources.ForEach (source => { source.TrySetCanceled (); }); + } - public InflightData (string requestUrl, CancellationToken cancellationToken, HttpRequestMessage request) + public (InflightData inflight, CancellationData cancellation) Create (NSUrlSessionTask dataTask, string requestUrl, HttpRequestMessage request, CancellationToken cancellationToken) { + var inflightData = new InflightData (request.RequestUri?.AbsoluteUri!, request); + var cancellationData = new CancellationData (cancellationToken); + + lock (Lock) { +#if !MONOMAC && !__WATCHOS__ + // Add the notification whenever needed + sessionHandler.AddNotification (); +#endif + inflightRequests.Add (dataTask, new (inflightData, cancellationData)); + } + + // 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 (() => { + Remove (dataTask); + cancellationData.CompletionSource.TrySetCanceled (); + }); + + return (inflightData, cancellationData); + } + + public void Get (NSUrlSessionTask task, out InflightData? inflightData, out CancellationData? cancellationData) { - RequestUrl = requestUrl; + lock (Lock) { + if (inflightRequests.TryGetValue (task, out var inflight)) { + // ensure that we did not cancel the request, if we did, do cancel the task, if we + // cancel the task it means that we are not interested in any of the delegate methods: + // + // DidReceiveResponse We might have received a response, but either the user cancelled or a + // timeout did, if that is the case, we do not care about the response. + // DidReceiveData Of buffer has a partial response ergo garbage and there is not real + // reason we would like to add more data. + // DidCompleteWithError - We are not changing a behaviour compared to the case in which + // we did not find the data. + (inflightData, cancellationData) = inflight; + if (cancellationData.CancellationToken.IsCancellationRequested) { + task?.Cancel (); + // return null so that we break out of any delegate method, but we do have the cancellation data + inflightData = null; + } + } else { + // 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 (); + inflightData = null; + cancellationData = null; + } + } + } + + public void Remove (NSUrlSessionTask task, bool cancel = true) + { + lock (Lock) { + if (inflightRequests.TryGetValue (task, out var inflight)) { + var (inflightData, cancellationData) = inflight; + if (cancel) + cancellationData.CancellationTokenSource.Cancel (); + cancellationData.Dispose (); + inflightRequests.Remove (task); + } +#if !MONOMAC && !__WATCHOS__ + // do we need to be notified? If we have not inflightData, we do not + if (inflightRequests.Count == 0) + sessionHandler.RemoveNotification (); +#endif + if (cancel) + task?.Cancel (); + + task?.Dispose (); + } + + } + + protected void Dispose (bool disposing) + { + lock (Lock) { +#if !MONOMAC && !__WATCHOS__ + // remove the notification if present, method checks against null + sessionHandler.RemoveNotification (); +#endif + foreach (var pair in inflightRequests) { + pair.Key?.Cancel (); + pair.Key?.Dispose (); + var (_, cancellation) = pair.Value; + cancellation.Dispose (); + } + + inflightRequests.Clear (); + } + } + + public void Dispose() + { + Dispose (true); + GC.SuppressFinalize(this); + } + + } + + class CancellationData : IDisposable { + public TaskCompletionSource CompletionSource { get; } = new TaskCompletionSource (TaskCreationOptions.RunContinuationsAsynchronously); + public CancellationToken CancellationToken { get; set; } + public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource (); + + public CancellationData (CancellationToken cancellationToken) { CancellationToken = cancellationToken; - Request = request; } public void Dispose() @@ -44,14 +165,39 @@ public void Dispose() GC.SuppressFinalize(this); } - // The bulk of the clean-up code is implemented in Dispose(bool) protected virtual void Dispose (bool disposing) { if (disposing) { CancellationTokenSource.Dispose (); } } + } + + // Contains the data of the infligh requests. Should not contain any reference to the cancellation objects, thos + // are shared by the managed code and we want to make sure that we have a clear separation between the managed and the + // unmanaged worlds. + class InflightData + { + public readonly object Lock = new object (); + public string RequestUrl { get; set; } + + public NSUrlSessionDataTaskStream Stream { get; } = new NSUrlSessionDataTaskStream (); + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage? Response { get; set; } + public Exception? Exception { get; set; } + public bool ResponseSent { get; set; } + public bool Errored { get; set; } + public bool Disposed { get; set; } + public bool Completed { get; set; } + // CancellationToken.IsCancellationRequested + public bool Done { get { return Errored || Disposed || Completed; } } + + public InflightData (string requestUrl, HttpRequestMessage request) + { + RequestUrl = requestUrl; + Request = request; + } } } diff --git a/src/Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs b/src/Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs index d83847f585b..21439813fed 100644 --- a/src/Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs +++ b/src/Foundation/NSUrlSessionHandler/NSUrlSessionHandlerDelegate.cs @@ -54,40 +54,11 @@ static Exception createExceptionForNSError(NSError error) return new HttpRequestException (error.LocalizedDescription, innerException); } - - 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 we - // cancel the task it means that we are not interested in any of the delegate methods: - // - // DidReceiveResponse We might have received a response, but either the user cancelled or a - // timeout did, if that is the case, we do not care about the response. - // DidReceiveData Of buffer has a partial response ergo garbage and there is not real - // reason we would like to add more data. - // DidCompleteWithError - We are not changing a behaviour compared to the case in which - // we did not find the data. - if (inflight.CancellationToken.IsCancellationRequested) { - task?.Cancel (); - // return null so that we break out of any delegate method. - return null; - } - 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; - } - void UpdateManagedCookieContainer (NSUrl url, NSHttpCookie[] cookies) { var uri = new Uri (url.AbsoluteString); if (sessionHandler.cookieContainer is not null && cookies.Length > 0) - lock (sessionHandler.inflightRequestsLock) { // ensure we lock when writing to the collection + lock (sessionHandler.inflightRequests.Lock) { // ensure we lock when writing to the collection var cookiesContents = new string [cookies.Length]; for (var index = 0; index < cookies.Length; index++) cookiesContents [index] = cookies [index].GetHeaderValue (); @@ -98,9 +69,9 @@ void UpdateManagedCookieContainer (NSUrl url, NSHttpCookie[] cookies) [Preserve (Conditional = true)] public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action completionHandler) { - var inflight = GetInflightData (dataTask); + sessionHandler.inflightRequests.Get (dataTask, out var inflight, out var cancellation); - if (inflight is null) + if (inflight is null || cancellation is null) return; try { @@ -115,8 +86,8 @@ public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataT inflight.Disposed = true; inflight.Stream.TrySetException (new ObjectDisposedException ("The content stream was disposed.")); - sessionHandler.RemoveInflightData (dataTask); - }, inflight.CancellationTokenSource.Token); + sessionHandler.inflightRequests.Remove (dataTask); + }, cancellation.CancellationTokenSource.Token); // NB: The double cast is because of a Xamarin compiler bug var httpResponse = new HttpResponseMessage ((HttpStatusCode)status) { @@ -155,10 +126,10 @@ public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataT dataTask.Resume (); } catch (Exception ex) { - inflight.CompletionSource.TrySetException (ex); + cancellation.CompletionSource.TrySetException (ex); inflight.Stream.TrySetException (ex); - sessionHandler.RemoveInflightData (dataTask); + sessionHandler.inflightRequests.Remove (dataTask); } completionHandler (NSUrlSessionResponseDisposition.Allow); @@ -167,69 +138,67 @@ public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataT [Preserve (Conditional = true)] public override void DidReceiveData (NSUrlSession session, NSUrlSessionDataTask dataTask, NSData data) { - var inflight = GetInflightData (dataTask); + sessionHandler.inflightRequests.Get (dataTask, out var inflight, out var cancellation); - if (inflight is null) + if (inflight is null || cancellation is null) return; inflight.Stream.Add (data); - SetResponse (inflight); + SetResponse (inflight, cancellation); } [Preserve (Conditional = true)] public override void DidCompleteWithError (NSUrlSession session, NSUrlSessionTask task, NSError? error) { - var inflight = GetInflightData (task); + sessionHandler.inflightRequests.Get (task, out var inflight, out var cancellation); var serverError = task.Error; // this can happen if the HTTP request times out and it is removed as part of the cancellation process - if (inflight is not null) { + if (inflight is not null && cancellation is not null) { // set the stream as finished inflight.Stream.TrySetReceivedAllData (); // send the error or send the response back if (error is not null || serverError is not null) { // got an error, cancel the stream operatios before we do anything - inflight.CancellationTokenSource.Cancel (); + cancellation?.CancellationTokenSource.Cancel (); inflight.Errored = true; var exc = inflight.Exception ?? createExceptionForNSError (error ?? serverError!); // client errors wont happen if we get server errors - inflight.CompletionSource.TrySetException (exc); + cancellation?.CompletionSource.TrySetException (exc); inflight.Stream.TrySetException (exc); } else { inflight.Completed = true; - SetResponse (inflight); + SetResponse (inflight, cancellation); } - sessionHandler.RemoveInflightData (task, cancel: false); + sessionHandler.inflightRequests.Remove (task, cancel: false); } } - void SetResponse (InflightData inflight) + void SetResponse (InflightData inflight, CancellationData cancellation) { lock (inflight.Lock) { if (inflight.ResponseSent) return; - if (inflight.CancellationTokenSource.Token.IsCancellationRequested) + if (cancellation.CancellationTokenSource.Token.IsCancellationRequested) return; - if (inflight.CompletionSource.Task.IsCompleted) + if (cancellation.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!)); + cancellation.CompletionSource.TrySetResult (httpResponse!); } } [Preserve (Conditional = true)] public override void WillCacheResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSCachedUrlResponse proposedResponse, Action completionHandler) { - var inflight = GetInflightData (dataTask); + sessionHandler.inflightRequests.Get (dataTask, out var inflight, out var _); if (inflight is null) return; @@ -247,7 +216,7 @@ public override void WillPerformHttpRedirection (NSUrlSession session, NSUrlSess [Preserve (Conditional = true)] public override void DidReceiveChallenge (NSUrlSession session, NSUrlSessionTask task, NSUrlAuthenticationChallenge challenge, Action completionHandler) { - var inflight = GetInflightData (task); + sessionHandler.inflightRequests.Get (task, out var inflight, out var _); if (inflight is null) return; From 5d1e96c69b111138f8106ad38fe59d9892ea9578 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Fri, 12 Aug 2022 17:04:52 -0400 Subject: [PATCH 10/12] [Foundation] Remove not longer needed property. --- src/Foundation/NSUrlSessionHandler/InflightData.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Foundation/NSUrlSessionHandler/InflightData.cs b/src/Foundation/NSUrlSessionHandler/InflightData.cs index cc88f429b8d..8a1a4fab5f9 100644 --- a/src/Foundation/NSUrlSessionHandler/InflightData.cs +++ b/src/Foundation/NSUrlSessionHandler/InflightData.cs @@ -190,8 +190,6 @@ class InflightData public bool Errored { get; set; } public bool Disposed { get; set; } public bool Completed { get; set; } - // CancellationToken.IsCancellationRequested - public bool Done { get { return Errored || Disposed || Completed; } } public InflightData (string requestUrl, HttpRequestMessage request) { From 66642b722be08e79cb0619d39d03acacc70a9d41 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Fri, 12 Aug 2022 17:06:24 -0400 Subject: [PATCH 11/12] [Foundation] Move the handler with the rest of the source files. --- .../{ => NSUrlSessionHandler}/NSUrlSessionHandler.cs | 0 src/Makefile | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/Foundation/{ => NSUrlSessionHandler}/NSUrlSessionHandler.cs (100%) diff --git a/src/Foundation/NSUrlSessionHandler.cs b/src/Foundation/NSUrlSessionHandler/NSUrlSessionHandler.cs similarity index 100% rename from src/Foundation/NSUrlSessionHandler.cs rename to src/Foundation/NSUrlSessionHandler/NSUrlSessionHandler.cs diff --git a/src/Makefile b/src/Makefile index c0709e058fd..0192412da52 100644 --- a/src/Makefile +++ b/src/Makefile @@ -152,7 +152,7 @@ IOS_EXTRA_SOURCES = \ $(SHARED_SYSTEM_DRAWING_SOURCES) \ IOS_DOTNET_HTTP_SOURCES = \ - Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ @@ -164,7 +164,7 @@ IOS_DOTNET_HTTP_SOURCES = \ System.Net.Http/CFNetworkHandler.cs \ IOS_HTTP_SOURCES = \ - Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ @@ -549,7 +549,7 @@ MAC_CFNETWORK_SOURCES = \ CFNetwork/WorkerThread.cs MAC_HTTP_SOURCES = \ - Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ @@ -871,7 +871,7 @@ WATCHOS_EXTRA_CORE_SOURCES = \ System.Drawing/ColorKnownColorTypeForwarders.cs \ WATCHOS_HTTP_SOURCES = \ - Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ From 27d65321af3b0a2b12b14a0cc217459915efbaf2 Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Tue, 16 Aug 2022 07:52:26 -0400 Subject: [PATCH 12/12] Fix make file. --- src/Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Makefile b/src/Makefile index 0192412da52..897c0ffbd13 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1170,7 +1170,7 @@ TVOS_CORE_SOURCES += \ $(TVOS_EXTRA_CORE_SOURCES) \ TVOS_DOTNET_HTTP_SOURCES = \ - Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ @@ -1183,7 +1183,7 @@ TVOS_DOTNET_HTTP_SOURCES = \ $(SHARED_DESIGNER_CS) \ TVOS_HTTP_SOURCES = \ - Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ @@ -1403,7 +1403,7 @@ MACCATALYST_DOTNET_CORE_SOURCES += \ $(MACCATALYST_DOTNET_EXTRA_CORE_SOURCES) \ MACCATALYST_DOTNET_HTTP_SOURCES = \ - Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \ @@ -1416,7 +1416,7 @@ MACCATALYST_DOTNET_HTTP_SOURCES = \ $(SHARED_DESIGNER_CS) \ MACCATALYST_HTTP_SOURCES = \ - Foundation/NSUrlSessionHandler.cs \ + Foundation/NSUrlSessionHandler/NSUrlSessionHandler.cs \ Foundation/NSUrlSessionHandler/NSHttpCookieExtensions.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStream.cs \ Foundation/NSUrlSessionHandler/NSUrlSessionDataTaskStreamContent.cs \