diff --git a/mcs/class/System.Net.Http/System.Net.Http/HttpClientHandler.cs b/mcs/class/System.Net.Http/System.Net.Http/HttpClientHandler.cs index 7d154cd8fdc7..dae2cd2350ae 100644 --- a/mcs/class/System.Net.Http/System.Net.Http/HttpClientHandler.cs +++ b/mcs/class/System.Net.Http/System.Net.Http/HttpClientHandler.cs @@ -387,7 +387,7 @@ protected async internal override Task SendAsync (HttpReque } } - wrequest.ResendContentFactory = content.CopyTo; + wrequest.ResendContentFactory = content.CopyToAsync; using (var stream = await wrequest.GetRequestStreamAsync ().ConfigureAwait (false)) { await request.Content.CopyToAsync (stream).ConfigureAwait (false); diff --git a/mcs/class/System/Mono.Net.Security/MonoTlsStream.cs b/mcs/class/System/Mono.Net.Security/MonoTlsStream.cs index ba6d5197231e..52d1be27eec8 100644 --- a/mcs/class/System/Mono.Net.Security/MonoTlsStream.cs +++ b/mcs/class/System/Mono.Net.Security/MonoTlsStream.cs @@ -52,7 +52,7 @@ namespace Mono.Net.Security { class MonoTlsStream { -#if SECURITY_DEP +#if SECURITY_DEP readonly MonoTlsProvider provider; readonly NetworkStream networkStream; readonly HttpWebRequest request; @@ -99,9 +99,11 @@ public MonoTlsStream (HttpWebRequest request, NetworkStream networkStream) #endif } - internal Stream CreateStream (byte[] buffer) + internal async Task CreateStream (WebConnectionTunnel tunnel, CancellationToken cancellationToken) { #if SECURITY_DEP + var socket = networkStream.InternalSocket; + WebConnection.Debug ($"MONO TLS STREAM CREATE STREAM: {socket.ID}"); sslStream = provider.CreateSslStream (networkStream, false, settings); try { @@ -112,16 +114,21 @@ internal Stream CreateStream (byte[] buffer) host = host.Substring (0, pos); } - sslStream.AuthenticateAsClient ( + await sslStream.AuthenticateAsClientAsync ( host, request.ClientCertificates, (SslProtocols)ServicePointManager.SecurityProtocol, - ServicePointManager.CheckCertificateRevocationList); + ServicePointManager.CheckCertificateRevocationList).ConfigureAwait (false); status = WebExceptionStatus.Success; - } catch { - status = WebExceptionStatus.SecureChannelFailure; + } catch (Exception ex) { + WebConnection.Debug ($"MONO TLS STREAM ERROR: {socket.ID} {socket.CleanedUp} {ex.Message}"); + if (socket.CleanedUp) + status = WebExceptionStatus.RequestCanceled; + else + status = WebExceptionStatus.SecureChannelFailure; throw; } finally { + WebConnection.Debug ($"MONO TLS STREAM CREATE STREAM DONE: {socket.ID} {socket.CleanedUp}"); if (CertificateValidationFailed) status = WebExceptionStatus.TrustFailure; @@ -134,8 +141,8 @@ internal Stream CreateStream (byte[] buffer) } try { - if (buffer != null) - sslStream.Write (buffer, 0, buffer.Length); + if (tunnel?.Data != null) + await sslStream.WriteAsync (tunnel.Data, 0, tunnel.Data.Length, cancellationToken).ConfigureAwait (false); } catch { status = WebExceptionStatus.SendFailure; sslStream = null; diff --git a/mcs/class/System/ReferenceSources/SR2.cs b/mcs/class/System/ReferenceSources/SR2.cs index 896362d98a4b..72c27496c3bf 100644 --- a/mcs/class/System/ReferenceSources/SR2.cs +++ b/mcs/class/System/ReferenceSources/SR2.cs @@ -15,4 +15,6 @@ partial class SR public const string net_log_set_socketoption_reuseport_default_on = "net_log_set_socketoption_reuseport_default_on"; public const string net_log_set_socketoption_reuseport_not_supported = "net_log_set_socketoption_reuseport_not_supported"; public const string net_log_set_socketoption_reuseport = "net_log_set_socketoption_reuseport"; + + public const string net_reqaborted = "The request was aborted: The request was canceled."; } diff --git a/mcs/class/System/System.Net.Sockets/Socket.cs b/mcs/class/System/System.Net.Sockets/Socket.cs index 8de0daf46ca1..1012e951f777 100644 --- a/mcs/class/System/System.Net.Sockets/Socket.cs +++ b/mcs/class/System/System.Net.Sockets/Socket.cs @@ -47,7 +47,7 @@ using System.Timers; using System.Net.NetworkInformation; -namespace System.Net.Sockets +namespace System.Net.Sockets { public partial class Socket : IDisposable { @@ -91,7 +91,14 @@ public partial class Socket : IDisposable int m_IntCleanedUp; internal bool connect_in_progress; -#region Constructors +#if MONO_WEB_DEBUG + static int nextId; + internal readonly int ID = ++nextId; +#else + internal readonly int ID; +#endif + + #region Constructors public Socket (SocketInformation socketInformation) diff --git a/mcs/class/System/System.Net/HttpWebRequest.cs b/mcs/class/System/System.Net/HttpWebRequest.cs index d05722051f93..e54736439e20 100644 --- a/mcs/class/System/System.Net/HttpWebRequest.cs +++ b/mcs/class/System/System.Net/HttpWebRequest.cs @@ -4,10 +4,12 @@ // Authors: // Lawrence Pit (loz@cable.a2000.nl) // Gonzalo Paniagua Javier (gonzalo@ximian.com) +// Martin Baulig // // (c) 2002 Lawrence Pit // (c) 2003 Ximian, Inc. (http://www.ximian.com) // (c) 2004 Novell, Inc. (http://www.novell.com) +// Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com) // // @@ -30,7 +32,6 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // - #if SECURITY_DEP #if MONO_SECURITY_ALIAS extern alias MonoSecurity; @@ -54,12 +55,14 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; +using System.Threading.Tasks; using Mono.Net.Security; -namespace System.Net +namespace System.Net { [Serializable] - public class HttpWebRequest : WebRequest, ISerializable { + public class HttpWebRequest : WebRequest, ISerializable + { Uri requestUri; Uri actualUri; bool hostChanged; @@ -72,8 +75,7 @@ public class HttpWebRequest : WebRequest, ISerializable { HttpContinueDelegate continueDelegate; CookieContainer cookieContainer; ICredentials credentials; - bool haveResponse; - bool haveRequest; + bool haveResponse; bool requestSent; WebHeaderCollection webHeaders; bool keepAlive = true; @@ -91,23 +93,18 @@ public class HttpWebRequest : WebRequest, ISerializable { bool sendChunked; ServicePoint servicePoint; int timeout = 100000; - - WebConnectionStream writeStream; + + WebRequestStream writeStream; HttpWebResponse webResponse; - WebAsyncResult asyncWrite; - WebAsyncResult asyncRead; - EventHandler abortHandler; + TaskCompletionSource responseTask; + WebOperation currentOperation; int aborted; bool gotRequestStream; int redirects; bool expectContinue; - byte[] bodyBuffer; - int bodyBufferLength; bool getResponseCalled; - Exception saved_exc; object locker = new object (); bool finished_reading; - internal WebConnection WebConnection; DecompressionMethods auto_decomp; int maxResponseHeadersLength; static int defaultMaxResponseHeadersLength; @@ -118,7 +115,8 @@ public class HttpWebRequest : WebRequest, ISerializable { #endif ServerCertValidationCallback certValidationCallback; - enum NtlmAuthState { + enum NtlmAuthState + { None, Challenge, Response @@ -127,7 +125,7 @@ enum NtlmAuthState { string host; [NonSerialized] - internal Action ResendContentFactory; + internal Func ResendContentFactory; // Constructors static HttpWebRequest () @@ -152,7 +150,7 @@ static HttpWebRequest () #else internal #endif - HttpWebRequest (Uri uri) + HttpWebRequest (Uri uri) { this.requestUri = uri; this.actualUri = uri; @@ -170,28 +168,28 @@ internal HttpWebRequest (Uri uri, MonoTlsProvider tlsProvider, MonoTlsSettings s this.tlsSettings = settings; } #endif - + [Obsolete ("Serialization is obsoleted for this type", false)] - protected HttpWebRequest (SerializationInfo serializationInfo, StreamingContext streamingContext) + protected HttpWebRequest (SerializationInfo serializationInfo, StreamingContext streamingContext) { SerializationInfo info = serializationInfo; - requestUri = (Uri) info.GetValue ("requestUri", typeof (Uri)); - actualUri = (Uri) info.GetValue ("actualUri", typeof (Uri)); + requestUri = (Uri)info.GetValue ("requestUri", typeof (Uri)); + actualUri = (Uri)info.GetValue ("actualUri", typeof (Uri)); allowAutoRedirect = info.GetBoolean ("allowAutoRedirect"); allowBuffering = info.GetBoolean ("allowBuffering"); - certificates = (X509CertificateCollection) info.GetValue ("certificates", typeof (X509CertificateCollection)); + certificates = (X509CertificateCollection)info.GetValue ("certificates", typeof (X509CertificateCollection)); connectionGroup = info.GetString ("connectionGroup"); contentLength = info.GetInt64 ("contentLength"); - webHeaders = (WebHeaderCollection) info.GetValue ("webHeaders", typeof (WebHeaderCollection)); + webHeaders = (WebHeaderCollection)info.GetValue ("webHeaders", typeof (WebHeaderCollection)); keepAlive = info.GetBoolean ("keepAlive"); maxAutoRedirect = info.GetInt32 ("maxAutoRedirect"); mediaType = info.GetString ("mediaType"); method = info.GetString ("method"); initialMethod = info.GetString ("initialMethod"); pipelined = info.GetBoolean ("pipelined"); - version = (Version) info.GetValue ("version", typeof (Version)); - proxy = (IWebProxy) info.GetValue ("proxy", typeof (IWebProxy)); + version = (Version)info.GetValue ("version", typeof (Version)); + proxy = (IWebProxy)info.GetValue ("proxy", typeof (IWebProxy)); sendChunked = info.GetBoolean ("sendChunked"); timeout = info.GetInt32 ("timeout"); redirects = info.GetInt32 ("redirects"); @@ -199,45 +197,53 @@ protected HttpWebRequest (SerializationInfo serializationInfo, StreamingContext ResetAuthorization (); } +#if MONO_WEB_DEBUG + static int nextId; + internal readonly int ID = ++nextId; +#else + internal readonly int ID; +#endif + void ResetAuthorization () { auth_state = new AuthorizationState (this, false); proxy_auth_state = new AuthorizationState (this, true); } - + // Properties - void SetSpecialHeaders(string HeaderName, string value) { - value = WebHeaderCollection.CheckBadChars(value, true); - webHeaders.RemoveInternal(HeaderName); + void SetSpecialHeaders (string HeaderName, string value) + { + value = WebHeaderCollection.CheckBadChars (value, true); + webHeaders.RemoveInternal (HeaderName); if (value.Length != 0) { - webHeaders.AddInternal(HeaderName, value); + webHeaders.AddInternal (HeaderName, value); } } public string Accept { - get { return webHeaders ["Accept"]; } + get { return webHeaders["Accept"]; } set { CheckRequestStarted (); SetSpecialHeaders ("Accept", value); } } - + public Uri Address { get { return actualUri; } internal set { actualUri = value; } // Used by Ftp+proxy } - + public virtual bool AllowAutoRedirect { get { return allowAutoRedirect; } set { this.allowAutoRedirect = value; } } - + public virtual bool AllowWriteStreamBuffering { get { return allowBuffering; } set { allowBuffering = value; } } - + public virtual bool AllowReadStreamBuffering { get { return false; } set { @@ -250,9 +256,8 @@ static Exception GetMustImplement () { return new NotImplementedException (); } - - public DecompressionMethods AutomaticDecompression - { + + public DecompressionMethods AutomaticDecompression { get { return auto_decomp; } @@ -261,7 +266,7 @@ public DecompressionMethods AutomaticDecompression auto_decomp = value; } } - + internal bool InternalAllowBuffering { get { return allowBuffering && MethodWithBuffer; @@ -300,7 +305,7 @@ public X509CertificateCollection ClientCertificates { } public string Connection { - get { return webHeaders ["Connection"]; } + get { return webHeaders["Connection"]; } set { CheckRequestStarted (); @@ -315,59 +320,59 @@ public string Connection { if (keepAlive) value = value + ", Keep-Alive"; - + webHeaders.CheckUpdate ("Connection", value); } - } - - public override string ConnectionGroupName { + } + + public override string ConnectionGroupName { get { return connectionGroup; } set { connectionGroup = value; } } - - public override long ContentLength { + + public override long ContentLength { get { return contentLength; } - set { + set { CheckRequestStarted (); if (value < 0) throw new ArgumentOutOfRangeException ("value", "Content-Length must be >= 0"); - + contentLength = value; haveContentLength = true; } } - + internal long InternalContentLength { set { contentLength = value; } } - + internal bool ThrowOnError { get; set; } - - public override string ContentType { - get { return webHeaders ["Content-Type"]; } + + public override string ContentType { + get { return webHeaders["Content-Type"]; } set { SetSpecialHeaders ("Content-Type", value); } } - + public HttpContinueDelegate ContinueDelegate { get { return continueDelegate; } set { continueDelegate = value; } } - + virtual public CookieContainer CookieContainer { get { return cookieContainer; } set { cookieContainer = value; } } - - public override ICredentials Credentials { + + public override ICredentials Credentials { get { return credentials; } set { credentials = value; } } public DateTime Date { get { - string date = webHeaders ["Date"]; + string date = webHeaders["Date"]; if (date == null) return DateTime.MinValue; return DateTime.ParseExact (date, "r", CultureInfo.InvariantCulture).ToLocalTime (); @@ -377,17 +382,17 @@ public DateTime Date { } } - void SetDateHeaderHelper(string headerName, DateTime dateTime) { + void SetDateHeaderHelper (string headerName, DateTime dateTime) + { if (dateTime == DateTime.MinValue) - SetSpecialHeaders(headerName, null); // remove header + SetSpecialHeaders (headerName, null); // remove header else - SetSpecialHeaders(headerName, HttpProtocolUtils.date2string(dateTime)); + SetSpecialHeaders (headerName, HttpProtocolUtils.date2string (dateTime)); } #if !MOBILE [MonoTODO] - public static new RequestCachePolicy DefaultCachePolicy - { + public static new RequestCachePolicy DefaultCachePolicy { get { throw GetMustImplement (); } @@ -396,10 +401,9 @@ void SetDateHeaderHelper(string headerName, DateTime dateTime) { } } #endif - + [MonoTODO] - public static int DefaultMaximumErrorResponseLength - { + public static int DefaultMaximumErrorResponseLength { get { throw GetMustImplement (); } @@ -407,9 +411,9 @@ public static int DefaultMaximumErrorResponseLength throw GetMustImplement (); } } - + public string Expect { - get { return webHeaders ["Expect"]; } + get { return webHeaders["Expect"]; } set { CheckRequestStarted (); string val = value; @@ -428,19 +432,19 @@ public string Expect { webHeaders.CheckUpdate ("Expect", value); } } - + virtual public bool HaveResponse { get { return haveResponse; } } - - public override WebHeaderCollection Headers { + + public override WebHeaderCollection Headers { get { return webHeaders; } set { CheckRequestStarted (); WebHeaderCollection webHeaders = value; - WebHeaderCollection newWebHeaders = new WebHeaderCollection(WebHeaderCollectionType.HttpWebRequest); + WebHeaderCollection newWebHeaders = new WebHeaderCollection (WebHeaderCollectionType.HttpWebRequest); // Copy And Validate - // Handle the case where their object tries to change @@ -448,16 +452,15 @@ public override WebHeaderCollection Headers { // we need to clone their headers. // - foreach (String headerName in webHeaders.AllKeys ) { - newWebHeaders.Add(headerName,webHeaders[headerName]); + foreach (String headerName in webHeaders.AllKeys) { + newWebHeaders.Add (headerName, webHeaders[headerName]); } this.webHeaders = newWebHeaders; } } - - public - string Host { + + public string Host { get { if (host == null) return actualUri.Authority; @@ -479,7 +482,7 @@ static bool CheckValidHost (string scheme, string val) if (val.Length == 0) return false; - if (val [0] == '.') + if (val[0] == '.') return false; int idx = val.IndexOf ('/'); @@ -495,8 +498,8 @@ static bool CheckValidHost (string scheme, string val) } public DateTime IfModifiedSince { - get { - string str = webHeaders ["If-Modified-Since"]; + get { + string str = webHeaders["If-Modified-Since"]; if (str == null) return DateTime.Now; try { @@ -508,13 +511,13 @@ public DateTime IfModifiedSince { set { CheckRequestStarted (); // rfc-1123 pattern - webHeaders.SetInternal ("If-Modified-Since", + webHeaders.SetInternal ("If-Modified-Since", value.ToUniversalTime ().ToString ("r", null)); // TODO: check last param when using different locale } } - public bool KeepAlive { + public bool KeepAlive { get { return keepAlive; } @@ -522,7 +525,7 @@ public bool KeepAlive { keepAlive = value; } } - + public int MaximumAutomaticRedirections { get { return maxAutoRedirect; } set { @@ -530,7 +533,7 @@ public int MaximumAutomaticRedirections { throw new ArgumentException ("Must be > 0", "value"); maxAutoRedirect = value; - } + } } [MonoTODO ("Use this")] @@ -545,7 +548,7 @@ public static int DefaultMaximumResponseHeadersLength { set { defaultMaxResponseHeadersLength = value; } } - public int ReadWriteTimeout { + public int ReadWriteTimeout { get { return readWriteTimeout; } set { if (requestSent) @@ -557,23 +560,23 @@ public int ReadWriteTimeout { readWriteTimeout = value; } } - + [MonoTODO] public int ContinueTimeout { get { throw new NotImplementedException (); } set { throw new NotImplementedException (); } } - + public string MediaType { get { return mediaType; } - set { + set { mediaType = value; } } - - public override string Method { + + public override string Method { get { return this.method; } - set { + set { if (value == null || value.Trim () == "") throw new ArgumentException ("not a valid method"); @@ -585,43 +588,43 @@ public override string Method { } } } - + public bool Pipelined { get { return pipelined; } set { pipelined = value; } - } - - public override bool PreAuthenticate { + } + + public override bool PreAuthenticate { get { return preAuthenticate; } set { preAuthenticate = value; } } - + public Version ProtocolVersion { get { return version; } - set { + set { if (value != HttpVersion.Version10 && value != HttpVersion.Version11) throw new ArgumentException ("value"); force_version = true; - version = value; + version = value; } } - - public override IWebProxy Proxy { + + public override IWebProxy Proxy { get { return proxy; } - set { + set { CheckRequestStarted (); proxy = value; servicePoint = null; // we may need a new one GetServicePoint (); } } - + public string Referer { - get { return webHeaders ["Referer"]; } + get { return webHeaders["Referer"]; } set { CheckRequestStarted (); - if (value == null || value.Trim().Length == 0) { + if (value == null || value.Trim ().Length == 0) { webHeaders.RemoveInternal ("Referer"); return; } @@ -629,10 +632,10 @@ public string Referer { } } - public override Uri RequestUri { + public override Uri RequestUri { get { return requestUri; } } - + public bool SendChunked { get { return sendChunked; } set { @@ -640,7 +643,7 @@ public bool SendChunked { sendChunked = value; } } - + public ServicePoint ServicePoint { get { return GetServicePoint (); } } @@ -648,14 +651,14 @@ public ServicePoint ServicePoint { internal ServicePoint ServicePointNoLock { get { return servicePoint; } } - public virtual bool SupportsCookieContainer { + public virtual bool SupportsCookieContainer { get { // The managed implementation supports the cookie container // it is only Silverlight that returns false here return true; } } - public override int Timeout { + public override int Timeout { get { return timeout; } set { if (value < -1) @@ -664,9 +667,9 @@ public override int Timeout { timeout = value; } } - + public string TransferEncoding { - get { return webHeaders ["Transfer-Encoding"]; } + get { return webHeaders["Transfer-Encoding"]; } set { CheckRequestStarted (); string val = value; @@ -688,20 +691,18 @@ public string TransferEncoding { } } - public override bool UseDefaultCredentials - { + public override bool UseDefaultCredentials { get { return CredentialCache.DefaultCredentials == Credentials; } set { Credentials = value ? CredentialCache.DefaultCredentials : null; } } - + public string UserAgent { - get { return webHeaders ["User-Agent"]; } + get { return webHeaders["User-Agent"]; } set { webHeaders.SetInternal ("User-Agent", value); } } bool unsafe_auth_blah; - public bool UnsafeAuthenticatedConnectionSharing - { + public bool UnsafeAuthenticatedConnectionSharing { get { return unsafe_auth_blah; } set { unsafe_auth_blah = value; } } @@ -714,11 +715,11 @@ internal bool ExpectContinue { get { return expectContinue; } set { expectContinue = value; } } - + internal Uri AuthUri { get { return actualUri; } } - + internal bool ProxyQuery { get { return servicePoint.UsesProxy && !servicePoint.UseConnect; } } @@ -733,8 +734,7 @@ public RemoteCertificateValidationCallback ServerCertificateValidationCallback { return null; return certValidationCallback.ValidationCallback; } - set - { + set { if (value == null) certValidationCallback = null; else @@ -743,7 +743,7 @@ public RemoteCertificateValidationCallback ServerCertificateValidationCallback { } // Methods - + internal ServicePoint GetServicePoint () { lock (locker) { @@ -755,30 +755,30 @@ internal ServicePoint GetServicePoint () return servicePoint; } - + public void AddRange (int range) { - AddRange ("bytes", (long) range); + AddRange ("bytes", (long)range); } - + public void AddRange (int from, int to) { - AddRange ("bytes", (long) from, (long) to); + AddRange ("bytes", (long)from, (long)to); } - + public void AddRange (string rangeSpecifier, int range) { - AddRange (rangeSpecifier, (long) range); + AddRange (rangeSpecifier, (long)range); } - + public void AddRange (string rangeSpecifier, int from, int to) { - AddRange (rangeSpecifier, (long) from, (long) to); + AddRange (rangeSpecifier, (long)from, (long)to); } public void AddRange (long range) { - AddRange ("bytes", (long) range); + AddRange ("bytes", (long)range); } public @@ -795,7 +795,7 @@ void AddRange (string rangeSpecifier, long range) if (!WebHeaderCollection.IsValidToken (rangeSpecifier)) throw new ArgumentException ("Invalid range specifier", "rangeSpecifier"); - string r = webHeaders ["Range"]; + string r = webHeaders["Range"]; if (r == null) r = rangeSpecifier + "="; else { @@ -825,7 +825,7 @@ void AddRange (string rangeSpecifier, long from, long to) if (to < 0) throw new ArgumentOutOfRangeException ("to"); - string r = webHeaders ["Range"]; + string r = webHeaders["Range"]; if (r == null) r = rangeSpecifier + "="; else @@ -835,11 +835,38 @@ void AddRange (string rangeSpecifier, long from, long to) webHeaders.ChangeInternal ("Range", r); } - - public override IAsyncResult BeginGetRequestStream (AsyncCallback callback, object state) + WebOperation SendRequest (bool redirecting, BufferOffsetSize writeBuffer, CancellationToken cancellationToken) + { + lock (locker) { + WebConnection.Debug ($"HWR SEND REQUEST: Req={ID} requestSent={requestSent} redirecting={redirecting}"); + + WebOperation operation; + if (!redirecting) { + if (requestSent) { + operation = currentOperation; + if (operation == null) + throw new InvalidOperationException ("Should never happen!"); + return operation; + } + } + + operation = new WebOperation (this, writeBuffer, false, cancellationToken); + if (Interlocked.CompareExchange (ref currentOperation, operation, null) != null) + throw new InvalidOperationException ("Invalid nested call."); + + requestSent = true; + if (!redirecting) + redirects = 0; + servicePoint = GetServicePoint (); + servicePoint.SendRequest (operation, connectionGroup); + return operation; + } + } + + async Task MyGetRequestStreamAsync (CancellationToken cancellationToken) { if (Aborted) - throw new WebException ("The request was canceled.", WebExceptionStatus.RequestCanceled); + throw CreateRequestAbortedException (); bool send = !(method == "GET" || method == "CONNECT" || method == "HEAD" || method == "TRACE"); @@ -853,36 +880,29 @@ public override IAsyncResult BeginGetRequestStream (AsyncCallback callback, obje if (!sendChunked && transferEncoding != null && transferEncoding.Trim () != "") throw new ProtocolViolationException ("SendChunked should be true."); - lock (locker) - { + WebOperation operation; + lock (locker) { if (getResponseCalled) throw new InvalidOperationException ("The operation cannot be performed once the request has been submitted."); - if (asyncWrite != null) { - throw new InvalidOperationException ("Cannot re-call start of asynchronous " + - "method while a previous call is still in progress."); - } - - asyncWrite = new WebAsyncResult (this, callback, state); - initialMethod = method; - if (haveRequest) { - if (writeStream != null) { - asyncWrite.SetCompleted (true, writeStream); - asyncWrite.DoCallback (); - return asyncWrite; - } - } - - gotRequestStream = true; - WebAsyncResult result = asyncWrite; - if (!requestSent) { - requestSent = true; - redirects = 0; - servicePoint = GetServicePoint (); - abortHandler = servicePoint.SendRequest (this, connectionGroup); + operation = currentOperation; + if (operation == null) { + initialMethod = method; + + gotRequestStream = true; + operation = SendRequest (false, null, cancellationToken); } - return result; } + + return await operation.GetRequestStream ().ConfigureAwait (false); + } + + public override IAsyncResult BeginGetRequestStream (AsyncCallback callback, object state) + { + if (Aborted) + throw CreateRequestAbortedException (); + + return TaskToApm.Begin (RunWithTimeout (MyGetRequestStreamAsync), callback, state); } public override Stream EndGetRequestStream (IAsyncResult asyncResult) @@ -890,34 +910,20 @@ public override Stream EndGetRequestStream (IAsyncResult asyncResult) if (asyncResult == null) throw new ArgumentNullException ("asyncResult"); - WebAsyncResult result = asyncResult as WebAsyncResult; - if (result == null) - throw new ArgumentException ("Invalid IAsyncResult"); - - asyncWrite = result; - result.WaitUntilComplete (); - - Exception e = result.Exception; - if (e != null) - throw e; - - return result.WriteStream; - } - - public override Stream GetRequestStream() - { - IAsyncResult asyncResult = asyncWrite; - if (asyncResult == null) { - asyncResult = BeginGetRequestStream (null, null); - asyncWrite = (WebAsyncResult) asyncResult; + try { + return TaskToApm.End (asyncResult); + } catch (Exception e) { + throw FlattenException (e); } + } - if (!asyncResult.IsCompleted && !asyncResult.AsyncWaitHandle.WaitOne (timeout, false)) { - Abort (); - throw new WebException ("The request timed out", WebExceptionStatus.Timeout); + public override Stream GetRequestStream () + { + try { + return GetRequestStreamAsync ().Result; + } catch (Exception e) { + throw FlattenException (e); } - - return EndGetRequestStream (asyncResult); } [MonoTODO] @@ -926,30 +932,34 @@ public Stream GetRequestStream (out TransportContext context) throw new NotImplementedException (); } - bool CheckIfForceWrite (SimpleAsyncResult result) + internal static async Task RunWithTimeout (Func> func, int timeout, Action abort) { - if (writeStream == null || writeStream.RequestWritten || !InternalAllowBuffering) - return false; - if (contentLength < 0 && writeStream.CanWrite == true && writeStream.WriteBufferLength < 0) - return false; - - if (contentLength < 0 && writeStream.WriteBufferLength >= 0) - InternalContentLength = writeStream.WriteBufferLength; - - // This will write the POST/PUT if the write stream already has the expected - // amount of bytes in it (ContentLength) (bug #77753) or if the write stream - // contains data and it has been closed already (xamarin bug #1512). - - if (writeStream.WriteBufferLength == contentLength || (contentLength == -1 && writeStream.CanWrite == false)) - return writeStream.WriteRequestAsync (result); + using (var cts = new CancellationTokenSource ()) { + var timeoutTask = Task.Delay (timeout); + var workerTask = func (cts.Token); + var ret = await Task.WhenAny (workerTask, timeoutTask).ConfigureAwait (false); + if (ret == timeoutTask) { + try { + cts.Cancel (); + abort (); + } catch { + // Ignore; we report the timeout. + } + throw new WebException (SR.net_timeout, WebExceptionStatus.Timeout); + } + return workerTask.Result; + } + } - return false; + Task RunWithTimeout (Func> func) + { + return RunWithTimeout (func, timeout, Abort); } - public override IAsyncResult BeginGetResponse (AsyncCallback callback, object state) + async Task MyGetResponseAsync (CancellationToken cancellationToken) { if (Aborted) - throw new WebException ("The request was canceled.", WebExceptionStatus.RequestCanceled); + throw CreateRequestAbortedException (); if (method == null) throw new ProtocolViolationException ("Method is null."); @@ -958,93 +968,232 @@ public override IAsyncResult BeginGetResponse (AsyncCallback callback, object st if (!sendChunked && transferEncoding != null && transferEncoding.Trim () != "") throw new ProtocolViolationException ("SendChunked should be true."); - Monitor.Enter (locker); - getResponseCalled = true; - if (asyncRead != null && !haveResponse) { - Monitor.Exit (locker); - throw new InvalidOperationException ("Cannot re-call start of asynchronous " + - "method while a previous call is still in progress."); + var myTcs = new TaskCompletionSource (); + WebOperation operation; + lock (locker) { + getResponseCalled = true; + var oldTcs = Interlocked.CompareExchange (ref responseTask, myTcs, null); + WebConnection.Debug ($"HWR GET RESPONSE: Req={ID} {oldTcs != null}"); + if (oldTcs != null) { + if (haveResponse && oldTcs.Task.IsCompleted) + return oldTcs.Task.Result; + throw new InvalidOperationException ("Cannot re-call start of asynchronous " + + "method while a previous call is still in progress."); + } + + operation = currentOperation; + if (currentOperation != null) + writeStream = currentOperation.WriteStream; + + initialMethod = method; + + operation = SendRequest (false, null, cancellationToken); } - asyncRead = new WebAsyncResult (this, callback, state); - WebAsyncResult aread = asyncRead; - initialMethod = method; + while (true) { + WebException throwMe = null; + HttpWebResponse response = null; + WebResponseStream stream = null; + bool redirect = false; + bool mustReadAll = false; + WebOperation ntlm = null; + BufferOffsetSize writeBuffer = null; + + try { + cancellationToken.ThrowIfCancellationRequested (); - SimpleAsyncResult.RunWithLock (locker, CheckIfForceWrite, inner => { - var synch = inner.CompletedSynchronouslyPeek; + WebConnection.Debug ($"HWR GET RESPONSE LOOP: Req={ID} {auth_state.NtlmAuthState}"); - if (inner.GotException) { - aread.SetCompleted (synch, inner.Exception); - aread.DoCallback (); - return; + writeStream = await operation.GetRequestStream (); + await writeStream.WriteRequestAsync (cancellationToken).ConfigureAwait (false); + + stream = await operation.GetResponseStream (); + + WebConnection.Debug ($"HWR RESPONSE LOOP #0: Req={ID} - {stream?.Headers != null}"); + + (response, redirect, mustReadAll, writeBuffer, ntlm) = await GetResponseFromData ( + stream, cancellationToken).ConfigureAwait (false); + } catch (Exception e) { + throwMe = GetWebException (e); } - if (haveResponse) { - Exception saved = saved_exc; - if (webResponse != null) { - if (saved == null) { - aread.SetCompleted (synch, webResponse); - } else { - aread.SetCompleted (synch, saved); - } - aread.DoCallback (); - return; - } else if (saved != null) { - aread.SetCompleted (synch, saved); - aread.DoCallback (); - return; + WebConnection.Debug ($"HWR GET RESPONSE LOOP #1: Req={ID} - redirect={redirect} mustReadAll={mustReadAll} writeBuffer={writeBuffer != null} ntlm={ntlm != null} - {throwMe != null}"); + + lock (locker) { + if (throwMe != null) { + WebConnection.Debug ($"HWR GET RESPONSE LOOP #1 EX: Req={ID} {throwMe.Status} {throwMe.InnerException?.GetType ()}"); + haveResponse = true; + myTcs.TrySetException (throwMe); + throw throwMe; } - } - if (requestSent) - return; + if (!redirect) { + haveResponse = true; + webResponse = response; + myTcs.TrySetResult (response); + return response; + } + + finished_reading = false; + haveResponse = false; + webResponse = null; + currentOperation = ntlm; + WebConnection.Debug ($"HWR GET RESPONSE LOOP #2: Req={ID} {mustReadAll} {ntlm}"); + } try { - requestSent = true; - redirects = 0; - servicePoint = GetServicePoint (); - abortHandler = servicePoint.SendRequest (this, connectionGroup); - } catch (Exception ex) { - aread.SetCompleted (synch, ex); - aread.DoCallback (); + if (mustReadAll) + await stream.ReadAllAsync (redirect || ntlm != null, cancellationToken).ConfigureAwait (false); + operation.CompleteResponseRead (true); + response.Close (); + } catch (Exception e) { + throwMe = GetWebException (e); } - }); - return aread; + lock (locker) { + WebConnection.Debug ($"HWR GET RESPONSE LOOP #3: Req={ID} {writeBuffer != null} {ntlm != null}"); + if (throwMe != null) { + WebConnection.Debug ($"HWR GET RESPONSE LOOP #3 EX: Req={ID} {throwMe.Status} {throwMe.InnerException?.GetType ()}"); + haveResponse = true; + stream?.Close (); + myTcs.TrySetException (throwMe); + throw throwMe; + } + + if (ntlm == null) { + operation = SendRequest (true, writeBuffer, cancellationToken); + } else { + operation = ntlm; + } + } + } } - public override WebResponse EndGetResponse (IAsyncResult asyncResult) + async Task<(HttpWebResponse response, bool redirect, bool mustReadAll, BufferOffsetSize writeBuffer, WebOperation ntlm)> + GetResponseFromData (WebResponseStream stream, CancellationToken cancellationToken) { - if (asyncResult == null) - throw new ArgumentNullException ("asyncResult"); + /* + * WebConnection has either called SetResponseData() or SetResponseError(). + */ + + var response = new HttpWebResponse (actualUri, method, stream, cookieContainer); - WebAsyncResult result = asyncResult as WebAsyncResult; - if (result == null) - throw new ArgumentException ("Invalid IAsyncResult", "asyncResult"); + WebException throwMe = null; + bool redirect = false; + bool mustReadAll = false; + WebOperation ntlm = null; + Task rewriteHandler = null; + BufferOffsetSize writeBuffer = null; - if (!result.WaitUntilComplete (timeout, false)) { - Abort (); - throw new WebException("The request timed out", WebExceptionStatus.Timeout); + lock (locker) { + (redirect, mustReadAll, rewriteHandler, throwMe) = CheckFinalStatus (response); } - if (result.GotException) - throw result.Exception; + if (throwMe != null) { + if (mustReadAll) + await stream.ReadAllAsync (false, cancellationToken).ConfigureAwait (false); + throw throwMe; + } + + if (rewriteHandler != null) { + writeBuffer = await rewriteHandler.ConfigureAwait (false); + } - return result.Response; + lock (locker) { + bool isProxy = ProxyQuery && proxy != null && !proxy.IsBypassed (actualUri); + + if (!redirect) { + if ((isProxy ? proxy_auth_state : auth_state).IsNtlmAuthenticated && (int)response.StatusCode < 400) { + stream.Connection.NtlmAuthenticated = true; + } + + // clear internal buffer so that it does not + // hold possible big buffer (bug #397627) + if (writeStream != null) + writeStream.KillBuffer (); + + return (response, false, false, writeBuffer, null); + } + + if (sendChunked) { + sendChunked = false; + webHeaders.RemoveInternal ("Transfer-Encoding"); + } + + bool isChallenge; + (ntlm, isChallenge) = HandleNtlmAuth (stream, response, writeBuffer, cancellationToken); + WebConnection.Debug ($"HWR REDIRECT: {ntlm} {isChallenge} {mustReadAll}"); + } + + return (response, true, mustReadAll, writeBuffer, ntlm); + } + + internal static Exception FlattenException (Exception e) + { + if (e is AggregateException ae) { + ae = ae.Flatten (); + if (ae.InnerExceptions.Count == 1) + return ae.InnerException; + } + + return e; + } + + WebException GetWebException (Exception e) + { + e = FlattenException (e); + if (e is WebException wexc) { + if (!Aborted || wexc.Status == WebExceptionStatus.RequestCanceled || wexc.Status == WebExceptionStatus.Timeout) + return wexc; + } + if (Aborted || e is OperationCanceledException || e is ObjectDisposedException) + return CreateRequestAbortedException (); + return new WebException (e.Message, e, WebExceptionStatus.UnknownError, null); + } + + internal static WebException CreateRequestAbortedException () + { + return new WebException (SR.Format (SR.net_reqaborted, WebExceptionStatus.RequestCanceled), WebExceptionStatus.RequestCanceled); } - + + public override IAsyncResult BeginGetResponse (AsyncCallback callback, object state) + { + if (Aborted) + throw CreateRequestAbortedException (); + + return TaskToApm.Begin (RunWithTimeout (MyGetResponseAsync), callback, state); + } + + public override WebResponse EndGetResponse (IAsyncResult asyncResult) + { + if (asyncResult == null) + throw new ArgumentNullException (nameof (asyncResult)); + + try { + return TaskToApm.End (asyncResult); + } catch (Exception e) { + throw FlattenException (e); + } + } + public Stream EndGetRequestStream (IAsyncResult asyncResult, out TransportContext context) { + if (asyncResult == null) + throw new ArgumentNullException (nameof (asyncResult)); + context = null; return EndGetRequestStream (asyncResult); } - public override WebResponse GetResponse() + public override WebResponse GetResponse () { - WebAsyncResult result = (WebAsyncResult) BeginGetResponse (null, null); - return EndGetResponse (result); + try { + return GetResponseAsync ().Result; + } catch (Exception e) { + throw FlattenException (e); + } } - + internal bool FinishedReading { get { return finished_reading; } set { finished_reading = value; } @@ -1059,56 +1208,23 @@ public override void Abort () if (Interlocked.CompareExchange (ref aborted, 1, 0) == 1) return; - if (haveResponse && finished_reading) - return; + WebConnection.Debug ($"HWR ABORT: Req={ID}"); haveResponse = true; - if (abortHandler != null) { - try { - abortHandler (this, EventArgs.Empty); - } catch (Exception) {} - abortHandler = null; - } - - if (asyncWrite != null) { - WebAsyncResult r = asyncWrite; - if (!r.IsCompleted) { - try { - WebException wexc = new WebException ("Aborted.", WebExceptionStatus.RequestCanceled); - r.SetCompleted (false, wexc); - r.DoCallback (); - } catch {} - } - asyncWrite = null; - } - - if (asyncRead != null) { - WebAsyncResult r = asyncRead; - if (!r.IsCompleted) { - try { - WebException wexc = new WebException ("Aborted.", WebExceptionStatus.RequestCanceled); - r.SetCompleted (false, wexc); - r.DoCallback (); - } catch {} - } - asyncRead = null; - } + var operation = currentOperation; + if (operation != null) + operation.Abort (); - if (writeStream != null) { - try { - writeStream.Close (); - writeStream = null; - } catch {} - } + responseTask?.TrySetCanceled (); if (webResponse != null) { try { webResponse.Close (); webResponse = null; - } catch {} + } catch { } } - } - + } + void ISerializable.GetObjectData (SerializationInfo serializationInfo, StreamingContext streamingContext) { @@ -1141,8 +1257,8 @@ protected override void GetObjectData (SerializationInfo serializationInfo, info.AddValue ("redirects", redirects); info.AddValue ("host", host); } - - void CheckRequestStarted () + + void CheckRequestStarted () { if (requestSent) throw new InvalidOperationException ("request started"); @@ -1160,8 +1276,8 @@ void RewriteRedirectToGet () webHeaders.RemoveInternal ("Transfer-Encoding"); sendChunked = false; } - - bool Redirect (WebAsyncResult result, HttpStatusCode code, WebResponse response) + + bool Redirect (HttpStatusCode code, WebResponse response) { redirects++; Exception e = null; @@ -1187,12 +1303,13 @@ bool Redirect (WebAsyncResult result, HttpStatusCode code, WebResponse response) break; case HttpStatusCode.Unused: // 306 default: - e = new ProtocolViolationException ("Invalid status code: " + (int) code); + e = new ProtocolViolationException ("Invalid status code: " + (int)code); break; } - if (method != "GET" && !InternalAllowBuffering && (writeStream.WriteBufferLength > 0 || contentLength > 0)) - e = new WebException ("The request requires buffering data to succeed.", null, WebExceptionStatus.ProtocolError, webResponse); + if (method != "GET" && !InternalAllowBuffering && ResendContentFactory == null && + (writeStream.WriteBufferLength > 0 || contentLength > 0)) + e = new WebException ("The request requires buffering data to succeed.", null, WebExceptionStatus.ProtocolError, response); if (e != null) throw e; @@ -1200,19 +1317,18 @@ bool Redirect (WebAsyncResult result, HttpStatusCode code, WebResponse response) if (AllowWriteStreamBuffering || method == "GET") contentLength = -1; - uriString = webResponse.Headers ["Location"]; + uriString = response.Headers["Location"]; if (uriString == null) - throw new WebException ("No Location header found for " + (int) code, - WebExceptionStatus.ProtocolError); + throw new WebException ($"No Location header found for {(int)code}", null, + WebExceptionStatus.ProtocolError, response); Uri prev = actualUri; try { actualUri = new Uri (actualUri, uriString); } catch (Exception) { - throw new WebException (String.Format ("Invalid URL ({0}) for {1}", - uriString, (int) code), - WebExceptionStatus.ProtocolError); + throw new WebException ($"Invalid URL ({uriString}) for {(int)code}", + null, WebExceptionStatus.ProtocolError, response); } hostChanged = (actualUri.Scheme != prev.Scheme || Host != prev.Authority); @@ -1247,7 +1363,7 @@ string GetHeaders () if (actualVersion == HttpVersion.Version11 && continue100 && servicePoint.SendContinue) { // RFC2616 8.2.3 - webHeaders.ChangeInternal ("Expect" , "100-continue"); + webHeaders.ChangeInternal ("Expect", "100-continue"); expectContinue = true; } else { webHeaders.RemoveInternal ("Expect"); @@ -1302,36 +1418,9 @@ void DoPreAuthenticate () webHeaders.RemoveInternal ("Proxy-Authorization"); webHeaders.RemoveInternal ("Authorization"); string authHeader = (isProxy && credentials == null) ? "Proxy-Authorization" : "Authorization"; - webHeaders [authHeader] = auth.Message; + webHeaders[authHeader] = auth.Message; usedPreAuth = true; } - - internal void SetWriteStreamError (WebExceptionStatus status, Exception exc) - { - if (Aborted) - return; - - WebAsyncResult r = asyncWrite; - if (r == null) - r = asyncRead; - - if (r != null) { - string msg; - WebException wex; - if (exc == null) { - msg = "Error: " + status; - wex = new WebException (msg, status); - } else { - wex = exc as WebException; - if (wex == null) { - msg = String.Format ("Error: {0} ({1})", status, exc.Message); - wex = new WebException (msg, status, WebExceptionInternalStatus.RequestFatal, exc); - } - } - r.SetCompleted (false, wex); - r.DoCallback (); - } - } internal byte[] GetRequestHeaders () { @@ -1340,11 +1429,11 @@ internal byte[] GetRequestHeaders () if (!ProxyQuery) { query = actualUri.PathAndQuery; } else { - query = String.Format ("{0}://{1}{2}", actualUri.Scheme, + query = String.Format ("{0}://{1}{2}", actualUri.Scheme, Host, actualUri.PathAndQuery); } - + if (!force_version && servicePoint.ProtocolVersion != null && servicePoint.ProtocolVersion < version) { actualVersion = servicePoint.ProtocolVersion; } else { @@ -1358,251 +1447,23 @@ internal byte[] GetRequestHeaders () return Encoding.UTF8.GetBytes (reqstr); } - internal void SetWriteStream (WebConnectionStream stream) - { - if (Aborted) - return; - - writeStream = stream; - if (bodyBuffer != null) { - webHeaders.RemoveInternal ("Transfer-Encoding"); - contentLength = bodyBufferLength; - writeStream.SendChunked = false; - } - - writeStream.SetHeadersAsync (false, result => { - if (result.GotException) { - SetWriteStreamError (result.Exception); - return; - } - - haveRequest = true; - - SetWriteStreamInner (inner => { - if (inner.GotException) { - SetWriteStreamError (inner.Exception); - return; - } - - if (asyncWrite != null) { - asyncWrite.SetCompleted (inner.CompletedSynchronouslyPeek, writeStream); - asyncWrite.DoCallback (); - asyncWrite = null; - } - }); - }); - } - - void SetWriteStreamInner (SimpleAsyncCallback callback) - { - SimpleAsyncResult.Run (result => { - if (bodyBuffer != null) { - // The body has been written and buffered. The request "user" - // won't write it again, so we must do it. - if (auth_state.NtlmAuthState != NtlmAuthState.Challenge && proxy_auth_state.NtlmAuthState != NtlmAuthState.Challenge) { - // FIXME: this is a blocking call on the thread pool that could lead to thread pool exhaustion - writeStream.Write (bodyBuffer, 0, bodyBufferLength); - bodyBuffer = null; - writeStream.Close (); - } - } else if (MethodWithBuffer) { - if (getResponseCalled && !writeStream.RequestWritten) - return writeStream.WriteRequestAsync (result); - } - - return false; - }, callback); - } - - void SetWriteStreamError (Exception exc) - { - WebException wexc = exc as WebException; - if (wexc != null) - SetWriteStreamError (wexc.Status, wexc); - else - SetWriteStreamError (WebExceptionStatus.SendFailure, exc); - } - - internal void SetResponseError (WebExceptionStatus status, Exception e, string where) - { - if (Aborted) - return; - lock (locker) { - string msg = String.Format ("Error getting response stream ({0}): {1}", where, status); - WebAsyncResult r = asyncRead; - if (r == null) - r = asyncWrite; - - WebException wexc; - if (e is WebException) { - wexc = (WebException) e; - } else { - wexc = new WebException (msg, e, status, null); - } - if (r != null) { - if (!r.IsCompleted) { - r.SetCompleted (false, wexc); - r.DoCallback (); - } else if (r == asyncWrite) { - saved_exc = wexc; - } - haveResponse = true; - asyncRead = null; - asyncWrite = null; - } else { - haveResponse = true; - saved_exc = wexc; - } - } - } - - void CheckSendError (WebConnectionData data) + (WebOperation, bool) HandleNtlmAuth (WebResponseStream stream, HttpWebResponse response, + BufferOffsetSize writeBuffer, CancellationToken cancellationToken) { - // Got here, but no one called GetResponse - int status = data.StatusCode; - if (status < 400 || status == 401 || status == 407) - return; - - if (writeStream != null && asyncRead == null && !writeStream.CompleteRequestWritten) { - // The request has not been completely sent and we got here! - // We should probably just close and cause an error in any case, - saved_exc = new WebException (data.StatusDescription, null, WebExceptionStatus.ProtocolError, webResponse); - if (allowBuffering || sendChunked || writeStream.totalWritten >= contentLength) { - webResponse.ReadAll (); - } else { - writeStream.IgnoreIOErrors = true; - } - } - } - - bool HandleNtlmAuth (WebAsyncResult r) - { - bool isProxy = webResponse.StatusCode == HttpStatusCode.ProxyAuthenticationRequired; - if ((isProxy ? proxy_auth_state.NtlmAuthState : auth_state.NtlmAuthState) == NtlmAuthState.None) - return false; - - WebConnectionStream wce = webResponse.GetResponseStream () as WebConnectionStream; - if (wce != null) { - WebConnection cnc = wce.Connection; - cnc.PriorityRequest = this; - ICredentials creds = (!isProxy || proxy == null) ? credentials : proxy.Credentials; - if (creds != null) { - cnc.NtlmCredential = creds.GetCredential (requestUri, "NTLM"); - cnc.UnsafeAuthenticatedConnectionSharing = unsafe_auth_blah; - } - } - r.Reset (); - finished_reading = false; - haveResponse = false; - webResponse.ReadAll (); - webResponse = null; - return true; - } - - internal void SetResponseData (WebConnectionData data) - { - lock (locker) { - if (Aborted) { - if (data.stream != null) - data.stream.Close (); - return; - } - - WebException wexc = null; - try { - webResponse = new HttpWebResponse (actualUri, method, data, cookieContainer); - } catch (Exception e) { - wexc = new WebException (e.Message, e, WebExceptionStatus.ProtocolError, null); - if (data.stream != null) - data.stream.Close (); - } - - if (wexc == null && (method == "POST" || method == "PUT")) { - CheckSendError (data); - if (saved_exc != null) - wexc = (WebException) saved_exc; - } - - WebAsyncResult r = asyncRead; - - bool forced = false; - if (r == null && webResponse != null) { - // This is a forced completion (302, 204)... - forced = true; - r = new WebAsyncResult (null, null); - r.SetCompleted (false, webResponse); - } - - if (r != null) { - if (wexc != null) { - haveResponse = true; - if (!r.IsCompleted) - r.SetCompleted (false, wexc); - r.DoCallback (); - return; - } + bool isProxy = response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired; + if ((isProxy ? proxy_auth_state : auth_state).NtlmAuthState == NtlmAuthState.None) + return (null, false); - bool isProxy = ProxyQuery && proxy != null && !proxy.IsBypassed (actualUri); + var isChallenge = auth_state.NtlmAuthState == NtlmAuthState.Challenge || proxy_auth_state.NtlmAuthState == NtlmAuthState.Challenge; - bool redirected; - try { - redirected = CheckFinalStatus (r); - if (!redirected) { - if ((isProxy ? proxy_auth_state.IsNtlmAuthenticated : auth_state.IsNtlmAuthenticated) && - webResponse != null && (int)webResponse.StatusCode < 400) { - WebConnectionStream wce = webResponse.GetResponseStream () as WebConnectionStream; - if (wce != null) { - WebConnection cnc = wce.Connection; - cnc.NtlmAuthenticated = true; - } - } - - // clear internal buffer so that it does not - // hold possible big buffer (bug #397627) - if (writeStream != null) - writeStream.KillBuffer (); - - haveResponse = true; - r.SetCompleted (false, webResponse); - r.DoCallback (); - } else { - if (sendChunked) { - sendChunked = false; - webHeaders.RemoveInternal ("Transfer-Encoding"); - } - - if (webResponse != null) { - if (HandleNtlmAuth (r)) - return; - webResponse.Close (); - } - finished_reading = false; - haveResponse = false; - webResponse = null; - r.Reset (); - servicePoint = GetServicePoint (); - abortHandler = servicePoint.SendRequest (this, connectionGroup); - } - } catch (WebException wexc2) { - if (forced) { - saved_exc = wexc2; - haveResponse = true; - } - r.SetCompleted (false, wexc2); - r.DoCallback (); - return; - } catch (Exception ex) { - wexc = new WebException (ex.Message, ex, WebExceptionStatus.ProtocolError, null); - if (forced) { - saved_exc = wexc; - haveResponse = true; - } - r.SetCompleted (false, wexc); - r.DoCallback (); - return; - } - } + var operation = new WebOperation (this, writeBuffer, isChallenge, cancellationToken); + stream.Operation.SetPriorityRequest (operation); + var creds = (!isProxy || proxy == null) ? credentials : proxy.Credentials; + if (creds != null) { + stream.Connection.NtlmCredential = creds.GetCredential (requestUri, "NTLM"); + stream.Connection.UnsafeAuthenticatedConnectionSharing = unsafe_auth_blah; } + return (operation, isChallenge); } struct AuthorizationState @@ -1645,7 +1506,7 @@ public bool CheckAuthorization (WebResponse response, HttpStatusCode code) if (isProxy && (request.proxy == null || request.proxy.Credentials == null)) return false; - string [] authHeaders = response.Headers.GetValues (isProxy ? "Proxy-Authenticate" : "WWW-Authenticate"); + string[] authHeaders = response.Headers.GetValues (isProxy ? "Proxy-Authenticate" : "WWW-Authenticate"); if (authHeaders == null || authHeaders.Length == 0) return false; @@ -1658,11 +1519,11 @@ public bool CheckAuthorization (WebResponse response, HttpStatusCode code) } if (auth == null) return false; - request.webHeaders [isProxy ? "Proxy-Authorization" : "Authorization"] = auth.Message; + request.webHeaders[isProxy ? "Proxy-Authorization" : "Authorization"] = auth.Message; isCompleted = auth.Complete; bool is_ntlm = (auth.ModuleAuthenticationType == "NTLM"); if (is_ntlm) - ntlm_auth_state = (NtlmAuthState)((int) ntlm_auth_state + 1); + ntlm_auth_state = (NtlmAuthState)((int)ntlm_auth_state + 1); return true; } @@ -1685,117 +1546,116 @@ bool CheckAuthorization (WebResponse response, HttpStatusCode code) return isProxy ? proxy_auth_state.CheckAuthorization (response, code) : auth_state.CheckAuthorization (response, code); } - // Returns true if redirected - bool CheckFinalStatus (WebAsyncResult result) + (Task task, WebException throwMe) GetRewriteHandler (HttpWebResponse response, bool redirect) { - if (result.GotException) { - bodyBuffer = null; - throw result.Exception; + if (redirect) { + if (!MethodWithBuffer) + return (null, null); + + if (writeStream.WriteBufferLength == 0 || contentLength == 0) + return (null, null); } - Exception throwMe = result.Exception; + // Keep the written body, so it can be rewritten in the retry + if (AllowWriteStreamBuffering) + return (Task.FromResult (writeStream.GetWriteBuffer ()), null); - HttpWebResponse resp = result.Response; - WebExceptionStatus protoError = WebExceptionStatus.ProtocolError; - HttpStatusCode code = 0; - if (throwMe == null && webResponse != null) { - code = webResponse.StatusCode; - if ((!auth_state.IsCompleted && code == HttpStatusCode.Unauthorized && credentials != null) || - (ProxyQuery && !proxy_auth_state.IsCompleted && code == HttpStatusCode.ProxyAuthenticationRequired)) { - if (!usedPreAuth && CheckAuthorization (webResponse, code)) { - // Keep the written body, so it can be rewritten in the retry - if (MethodWithBuffer) { - if (AllowWriteStreamBuffering) { - if (writeStream.WriteBufferLength > 0) { - bodyBuffer = writeStream.WriteBuffer; - bodyBufferLength = writeStream.WriteBufferLength; - } - - return true; - } - - // - // Buffering is not allowed but we have alternative way to get same content (we - // need to resent it due to NTLM Authentication). - // - if (ResendContentFactory != null) { - using (var ms = new MemoryStream ()) { - ResendContentFactory (ms); - bodyBuffer = ms.ToArray (); - bodyBufferLength = bodyBuffer.Length; - } - return true; - } - } else if (method != "PUT" && method != "POST") { - bodyBuffer = null; - return true; - } - - if (!ThrowOnError) - return false; - - writeStream.InternalClose (); - writeStream = null; - webResponse.Close (); - webResponse = null; - bodyBuffer = null; - - throw new WebException ("This request requires buffering " + - "of data for authentication or " + - "redirection to be sucessful."); - } + if (ResendContentFactory == null) + return (null, new WebException ( + "The request requires buffering data to succeed.", null, WebExceptionStatus.ProtocolError, response)); + + Func> handleResendContentFactory = async () => { + using (var ms = new MemoryStream ()) { + await ResendContentFactory (ms).ConfigureAwait (false); + var buffer = ms.ToArray (); + return new BufferOffsetSize (buffer, 0, buffer.Length, false); } + }; + + // + // Buffering is not allowed but we have alternative way to get same content (we + // need to resent it due to NTLM Authentication). + // + return (handleResendContentFactory (), null); + } + + // Returns true if redirected + (bool redirect, bool mustReadAll, Task writeBuffer, WebException throwMe) CheckFinalStatus (HttpWebResponse response) + { + WebException throwMe = null; + + bool mustReadAll = false; + HttpStatusCode code = 0; + Task rewriteHandler = null; + + code = response.StatusCode; + if ((!auth_state.IsCompleted && code == HttpStatusCode.Unauthorized && credentials != null) || + (ProxyQuery && !proxy_auth_state.IsCompleted && code == HttpStatusCode.ProxyAuthenticationRequired)) { + if (!usedPreAuth && CheckAuthorization (response, code)) { + mustReadAll = true; + + // HEAD, GET, MKCOL, CONNECT, TRACE + if (!MethodWithBuffer) + return (true, mustReadAll, null, null); + + (rewriteHandler, throwMe) = GetRewriteHandler (response, false); + if (throwMe == null) + return (true, mustReadAll, rewriteHandler, null); - bodyBuffer = null; - if ((int) code >= 400) { - string err = String.Format ("The remote server returned an error: ({0}) {1}.", - (int) code, webResponse.StatusDescription); - throwMe = new WebException (err, null, protoError, webResponse); - webResponse.ReadAll (); - } else if ((int) code == 304 && allowAutoRedirect) { - string err = String.Format ("The remote server returned an error: ({0}) {1}.", - (int) code, webResponse.StatusDescription); - throwMe = new WebException (err, null, protoError, webResponse); - } else if ((int) code >= 300 && allowAutoRedirect && redirects >= maxAutoRedirect) { - throwMe = new WebException ("Max. redirections exceeded.", null, - protoError, webResponse); - webResponse.ReadAll (); + if (!ThrowOnError) + return (false, mustReadAll, null, null); + + writeStream.InternalClose (); + writeStream = null; + response.Close (); + + return (false, mustReadAll, null, throwMe); } } - bodyBuffer = null; + if ((int)code >= 400) { + string err = String.Format ("The remote server returned an error: ({0}) {1}.", + (int)code, response.StatusDescription); + throwMe = new WebException (err, null, WebExceptionStatus.ProtocolError, response); + mustReadAll = true; + } else if ((int)code == 304 && allowAutoRedirect) { + string err = String.Format ("The remote server returned an error: ({0}) {1}.", + (int)code, response.StatusDescription); + throwMe = new WebException (err, null, WebExceptionStatus.ProtocolError, response); + } else if ((int)code >= 300 && allowAutoRedirect && redirects >= maxAutoRedirect) { + throwMe = new WebException ("Max. redirections exceeded.", null, + WebExceptionStatus.ProtocolError, response); + mustReadAll = true; + } + if (throwMe == null) { + int c = (int)code; bool b = false; - int c = (int) code; if (allowAutoRedirect && c >= 300) { - b = Redirect (result, code, webResponse); - if (InternalAllowBuffering && writeStream.WriteBufferLength > 0) { - bodyBuffer = writeStream.WriteBuffer; - bodyBufferLength = writeStream.WriteBufferLength; - } + b = Redirect (code, response); + (rewriteHandler, throwMe) = GetRewriteHandler (response, true); if (b && !unsafe_auth_blah) { auth_state.Reset (); proxy_auth_state.Reset (); } } - if (resp != null && c >= 300 && c != 304) - resp.ReadAll (); + if (c >= 300 && c != 304) + mustReadAll = true; - return b; + if (throwMe == null) + return (b, mustReadAll, rewriteHandler, null); } - + if (!ThrowOnError) - return false; + return (false, mustReadAll, null, null); if (writeStream != null) { writeStream.InternalClose (); writeStream = null; } - webResponse = null; - - throw throwMe; + return (false, mustReadAll, null, throwMe); } internal bool ReuseConnection { @@ -1803,8 +1663,6 @@ internal bool ReuseConnection { set; } - internal WebConnection StoredConnection; - #region referencesource internal static StringBuilder GenerateConnectionGroup(string connectionGroupName, bool unsafeConnectionGroup, bool isInternalGroup) { diff --git a/mcs/class/System/System.Net/HttpWebResponse.cs b/mcs/class/System/System.Net/HttpWebResponse.cs index 5a3208497235..c44949811c2f 100644 --- a/mcs/class/System/System.Net/HttpWebResponse.cs +++ b/mcs/class/System/System.Net/HttpWebResponse.cs @@ -39,6 +39,8 @@ using System.IO.Compression; using System.Net.Sockets; using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; using System.Text; namespace System.Net @@ -60,16 +62,28 @@ public class HttpWebResponse : WebResponse, ISerializable, IDisposable { Stream stream; // Constructors + + internal HttpWebResponse (Uri uri, string method, HttpStatusCode status, WebHeaderCollection headers) + { + this.uri = uri; + this.method = method; + this.statusCode = status; + this.statusDescription = HttpStatusDescription.Get (status); + this.webHeaders = headers; + version = HttpVersion.Version10; + contentLength = -1; + } - internal HttpWebResponse (Uri uri, string method, WebConnectionData data, CookieContainer container) + internal HttpWebResponse (Uri uri, string method, WebResponseStream stream, CookieContainer container) { this.uri = uri; this.method = method; - webHeaders = data.Headers; - version = data.Version; - statusCode = (HttpStatusCode) data.StatusCode; - statusDescription = data.StatusDescription; - stream = data.stream; + this.stream = stream; + + webHeaders = stream.Headers ?? new WebHeaderCollection (); + version = stream.Version; + statusCode = stream.StatusCode; + statusDescription = stream.StatusDescription ?? HttpStatusDescription.Get (statusCode); contentLength = -1; try { @@ -86,12 +100,12 @@ internal HttpWebResponse (Uri uri, string method, WebConnectionData data, Cookie } string content_encoding = webHeaders ["Content-Encoding"]; - if (content_encoding == "gzip" && (data.request.AutomaticDecompression & DecompressionMethods.GZip) != 0) { - stream = new GZipStream (stream, CompressionMode.Decompress); + if (content_encoding == "gzip" && (stream.Request.AutomaticDecompression & DecompressionMethods.GZip) != 0) { + this.stream = new GZipStream (stream, CompressionMode.Decompress); webHeaders.Remove (HttpRequestHeader.ContentEncoding); } - else if (content_encoding == "deflate" && (data.request.AutomaticDecompression & DecompressionMethods.Deflate) != 0) { - stream = new DeflateStream (stream, CompressionMode.Decompress); + else if (content_encoding == "deflate" && (stream.Request.AutomaticDecompression & DecompressionMethods.Deflate) != 0) { + this.stream = new DeflateStream (stream, CompressionMode.Decompress); webHeaders.Remove (HttpRequestHeader.ContentEncoding); } } @@ -263,17 +277,6 @@ public string GetResponseHeader (string headerName) return (value != null) ? value : ""; } - internal void ReadAll () - { - WebConnectionStream wce = stream as WebConnectionStream; - if (wce == null) - return; - - try { - wce.ReadAll (); - } catch {} - } - public override Stream GetResponseStream () { CheckDisposed (); @@ -310,12 +313,9 @@ protected override void GetObjectData (SerializationInfo serializationInfo, public override void Close () { - if (stream != null) { - Stream st = stream; - stream = null; - if (st != null) - st.Close (); - } + var st = Interlocked.Exchange (ref stream, null); + if (st != null) + st.Close (); } void IDisposable.Dispose () diff --git a/mcs/class/System/System.Net/IWebConnectionState.cs b/mcs/class/System/System.Net/IWebConnectionState.cs deleted file mode 100644 index 7b032decbb7b..000000000000 --- a/mcs/class/System/System.Net/IWebConnectionState.cs +++ /dev/null @@ -1,52 +0,0 @@ -// -// IWebConnectionState.cs -// -// Author: -// Martin Baulig -// -// Copyright (c) 2014 Xamarin Inc. (http://www.xamarin.com) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -using System; -using System.Threading; - -namespace System.Net -{ - interface IWebConnectionState { - WebConnectionGroup Group { - get; - } - - ServicePoint ServicePoint { - get; - } - - bool Busy { - get; - } - - DateTime IdleSince { - get; - } - - bool TrySetBusy (); - - void SetIdle (); - } -} diff --git a/mcs/class/System/System.Net/ServicePoint.cs b/mcs/class/System/System.Net/ServicePoint.cs index 9b6cc5cc5e4d..f8eee86caca9 100644 --- a/mcs/class/System/System.Net/ServicePoint.cs +++ b/mcs/class/System/System.Net/ServicePoint.cs @@ -38,20 +38,15 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; -namespace System.Net +namespace System.Net { public class ServicePoint { readonly Uri uri; - int connectionLimit; - int maxIdleTime; - int currentConnections; - DateTime idleSince; DateTime lastDnsResolve; Version protocolVersion; IPHostEntry host; bool usesProxy; - Dictionary groups; bool sendContinue = true; bool useConnect; object hostE = new object (); @@ -60,21 +55,22 @@ public class ServicePoint bool tcp_keepalive; int tcp_keepalive_time; int tcp_keepalive_interval; - Timer idleTimer; // Constructors internal ServicePoint (Uri uri, int connectionLimit, int maxIdleTime) { - this.uri = uri; - this.connectionLimit = connectionLimit; - this.maxIdleTime = maxIdleTime; - this.currentConnections = 0; - this.idleSince = DateTime.UtcNow; + this.uri = uri; + + Scheduler = new ServicePointScheduler (this, connectionLimit, maxIdleTime); + } + + internal ServicePointScheduler Scheduler { + get; } - + // Properties - + public Uri Address { get { return uri; } } @@ -84,15 +80,13 @@ static Exception GetMustImplement () return new NotImplementedException (); } - public BindIPEndPoint BindIPEndPointDelegate - { + public BindIPEndPoint BindIPEndPointDelegate { get { return endPointCallback; } set { endPointCallback = value; } } - + [MonoTODO] - public int ConnectionLeaseTimeout - { + public int ConnectionLeaseTimeout { get { throw GetMustImplement (); } @@ -100,54 +94,39 @@ public int ConnectionLeaseTimeout throw GetMustImplement (); } } - - public int ConnectionLimit { - get { return connectionLimit; } - set { - if (value <= 0) - throw new ArgumentOutOfRangeException (); - connectionLimit = value; - } + public int ConnectionLimit { + get { return Scheduler.ConnectionLimit; } + set { Scheduler.ConnectionLimit = value; } } - + public string ConnectionName { get { return uri.Scheme; } } public int CurrentConnections { get { - return currentConnections; + return Scheduler.CurrentConnections; } } public DateTime IdleSince { get { - return idleSince.ToLocalTime (); + return Scheduler.IdleSince.ToLocalTime (); } } public int MaxIdleTime { - get { return maxIdleTime; } - set { - if (value < Timeout.Infinite || value > Int32.MaxValue) - throw new ArgumentOutOfRangeException (); - - lock (this) { - maxIdleTime = value; - if (idleTimer != null) - idleTimer.Change (maxIdleTime, maxIdleTime); - } - } + get { return Scheduler.MaxIdleTime; } + set { Scheduler.MaxIdleTime = value; } } - + public virtual Version ProtocolVersion { get { return protocolVersion; } } [MonoTODO] - public int ReceiveBufferSize - { + public int ReceiveBufferSize { get { throw GetMustImplement (); } @@ -155,7 +134,7 @@ public int ReceiveBufferSize throw GetMustImplement (); } } - + public bool SupportsPipelining { get { return HttpVersion.Version11.Equals (protocolVersion); } } @@ -172,8 +151,10 @@ public bool UseNagleAlgorithm { } internal bool SendContinue { - get { return sendContinue && - (protocolVersion == null || protocolVersion == HttpVersion.Version11); } + get { + return sendContinue && + (protocolVersion == null || protocolVersion == HttpVersion.Version11); + } set { sendContinue = value; } } // Methods @@ -197,25 +178,25 @@ internal void KeepAliveSetup (Socket socket) if (!tcp_keepalive) return; - byte [] bytes = new byte [12]; - PutBytes (bytes, (uint) (tcp_keepalive ? 1 : 0), 0); - PutBytes (bytes, (uint) tcp_keepalive_time, 4); - PutBytes (bytes, (uint) tcp_keepalive_interval, 8); + byte[] bytes = new byte[12]; + PutBytes (bytes, (uint)(tcp_keepalive ? 1 : 0), 0); + PutBytes (bytes, (uint)tcp_keepalive_time, 4); + PutBytes (bytes, (uint)tcp_keepalive_interval, 8); socket.IOControl (IOControlCode.KeepAliveValues, bytes, null); } - static void PutBytes (byte [] bytes, uint v, int offset) + static void PutBytes (byte[] bytes, uint v, int offset) { if (BitConverter.IsLittleEndian) { - bytes [offset] = (byte) (v & 0x000000ff); - bytes [offset + 1] = (byte) ((v & 0x0000ff00) >> 8); - bytes [offset + 2] = (byte) ((v & 0x00ff0000) >> 16); - bytes [offset + 3] = (byte) ((v & 0xff000000) >> 24); + bytes[offset] = (byte)(v & 0x000000ff); + bytes[offset + 1] = (byte)((v & 0x0000ff00) >> 8); + bytes[offset + 2] = (byte)((v & 0x00ff0000) >> 16); + bytes[offset + 3] = (byte)((v & 0xff000000) >> 24); } else { - bytes [offset + 3] = (byte) (v & 0x000000ff); - bytes [offset + 2] = (byte) ((v & 0x0000ff00) >> 8); - bytes [offset + 1] = (byte) ((v & 0x00ff0000) >> 16); - bytes [offset] = (byte) ((v & 0xff000000) >> 24); + bytes[offset + 3] = (byte)(v & 0x000000ff); + bytes[offset + 2] = (byte)((v & 0x0000ff00) >> 8); + bytes[offset + 1] = (byte)((v & 0x00ff0000) >> 16); + bytes[offset] = (byte)((v & 0xff000000) >> 24); } } @@ -231,107 +212,7 @@ internal bool UseConnect { set { useConnect = value; } } - WebConnectionGroup GetConnectionGroup (string name) - { - if (name == null) - name = ""; - - /* - * Optimization: - * - * In the vast majority of cases, we only have one single WebConnectionGroup per ServicePoint, so we - * don't need to allocate a dictionary. - * - */ - - WebConnectionGroup group; - if (groups != null && groups.TryGetValue (name, out group)) - return group; - - group = new WebConnectionGroup (this, name); - group.ConnectionClosed += (s, e) => currentConnections--; - - if (groups == null) - groups = new Dictionary (); - groups.Add (name, group); - - return group; - } - - void RemoveConnectionGroup (WebConnectionGroup group) - { - if (groups == null || groups.Count == 0) - throw new InvalidOperationException (); - - groups.Remove (group.Name); - } - - bool CheckAvailableForRecycling (out DateTime outIdleSince) - { - outIdleSince = DateTime.MinValue; - - TimeSpan idleTimeSpan; - List groupList = null, removeList = null; - lock (this) { - if (groups == null || groups.Count == 0) { - idleSince = DateTime.MinValue; - return true; - } - - idleTimeSpan = TimeSpan.FromMilliseconds (maxIdleTime); - - /* - * WebConnectionGroup.TryRecycle() must run outside the lock, so we need to - * copy the group dictionary if it exists. - * - * In most cases, we only have a single connection group, so we can simply store - * that in a local variable instead of copying a collection. - * - */ - - groupList = new List (groups.Values); - } - - foreach (var group in groupList) { - if (!group.TryRecycle (idleTimeSpan, ref outIdleSince)) - continue; - if (removeList == null) - removeList = new List (); - removeList.Add (group); - } - - lock (this) { - idleSince = outIdleSince; - - if (removeList != null && groups != null) { - foreach (var group in removeList) - if (groups.ContainsKey (group.Name)) - RemoveConnectionGroup (group); - } - - if (groups != null && groups.Count == 0) - groups = null; - - if (groups == null) { - if (idleTimer != null) { - idleTimer.Dispose (); - idleTimer = null; - } - return true; - } - - return false; - } - } - - void IdleTimerCallback (object obj) - { - DateTime dummy; - CheckAvailableForRecycling (out dummy); - } - - private bool HasTimedOut - { + private bool HasTimedOut { get { int timeout = ServicePointManager.DnsRefreshTimeout; return timeout != Timeout.Infinite && @@ -339,8 +220,7 @@ private bool HasTimedOut } } - internal IPHostEntry HostEntry - { + internal IPHostEntry HostEntry { get { lock (hostE) { string uriHost = uri.Host; @@ -356,7 +236,7 @@ internal IPHostEntry HostEntry } // Creates IPHostEntry - host = new IPHostEntry(); + host = new IPHostEntry (); host.AddressList = new IPAddress[] { IPAddress.Parse (uriHost) }; return host; } @@ -382,41 +262,18 @@ internal void SetVersion (Version version) protocolVersion = version; } - internal EventHandler SendRequest (HttpWebRequest request, string groupName) + internal void SendRequest (WebOperation operation, string groupName) { - WebConnection cnc; - lock (this) { - bool created; - WebConnectionGroup cncGroup = GetConnectionGroup (groupName); - cnc = cncGroup.GetConnection (request, out created); - if (created) { - ++currentConnections; - if (idleTimer == null) - idleTimer = new Timer (IdleTimerCallback, null, maxIdleTime, maxIdleTime); - } + Scheduler.SendRequest (operation, groupName); } - - return cnc.SendRequest (request); } + public bool CloseConnectionGroup (string connectionGroupName) { - WebConnectionGroup cncGroup = null; - lock (this) { - cncGroup = GetConnectionGroup (connectionGroupName); - if (cncGroup != null) { - RemoveConnectionGroup (cncGroup); - } + return Scheduler.CloseConnectionGroup (connectionGroupName); } - - // WebConnectionGroup.Close() must *not* be called inside the lock - if (cncGroup != null) { - cncGroup.Close (); - return true; - } - - return false; } // diff --git a/mcs/class/System/System.Net/ServicePointScheduler.cs b/mcs/class/System/System.Net/ServicePointScheduler.cs new file mode 100644 index 000000000000..2e95656f759a --- /dev/null +++ b/mcs/class/System/System.Net/ServicePointScheduler.cs @@ -0,0 +1,621 @@ +// +// ServicePointScheduler.cs +// +// Author: +// Martin Baulig +// +// Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System.IO; +using System.Collections; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.ExceptionServices; +using System.Diagnostics; + +namespace System.Net +{ + class ServicePointScheduler + { + public ServicePoint ServicePoint { + get; + } + + public int MaxIdleTime { + get { return maxIdleTime; } + set { + if (value < Timeout.Infinite || value > Int32.MaxValue) + throw new ArgumentOutOfRangeException (); + if (value == maxIdleTime) + return; + maxIdleTime = value; + Debug ($"MAX IDLE TIME = {value}"); + Run (); + } + } + + public int ConnectionLimit { + get { return connectionLimit; } + set { + if (value <= 0) + throw new ArgumentOutOfRangeException (); + + if (value == connectionLimit) + return; + connectionLimit = value; + Debug ($"CONNECTION LIMIT = {value}"); + Run (); + } + } + + public ServicePointScheduler (ServicePoint servicePoint, int connectionLimit, int maxIdleTime) + { + ServicePoint = servicePoint; + this.connectionLimit = connectionLimit; + this.maxIdleTime = maxIdleTime; + + schedulerEvent = new AsyncManualResetEvent (false); + defaultGroup = new ConnectionGroup (this, string.Empty); + operations = new LinkedList<(ConnectionGroup, WebOperation)> (); + idleConnections = new LinkedList<(ConnectionGroup, WebConnection, Task)> (); + idleSince = DateTime.UtcNow; + } + + [Conditional ("MONO_WEB_DEBUG")] + void Debug (string message, params object[] args) + { + WebConnection.Debug ($"SPS({ID}): {string.Format (message, args)}"); + } + + [Conditional ("MONO_WEB_DEBUG")] + void Debug (string message) + { + WebConnection.Debug ($"SPS({ID}): {message}"); + } + + int running; + int maxIdleTime = 100000; + AsyncManualResetEvent schedulerEvent; + ConnectionGroup defaultGroup; + Dictionary groups; + LinkedList<(ConnectionGroup, WebOperation)> operations; + LinkedList<(ConnectionGroup, WebConnection, Task)> idleConnections; + int currentConnections; + int connectionLimit; + DateTime idleSince; + + public int CurrentConnections { + get { + return currentConnections; + } + } + + public DateTime IdleSince { + get { + return idleSince; + } + } + + static int nextId; + public readonly int ID = ++nextId; + + internal string ME { + get; + } + + public void Run () + { + lock (ServicePoint) { + if (Interlocked.CompareExchange (ref running, 1, 0) == 0) + StartScheduler (); + + schedulerEvent.Set (); + } + } + + async void StartScheduler () + { + idleSince = DateTime.UtcNow + TimeSpan.FromDays (3650); + + while (true) { + Debug ($"MAIN LOOP"); + + // Gather list of currently running operations. + ValueTuple[] operationArray; + ValueTuple[] idleArray; + var taskList = new List (); + lock (ServicePoint) { + Cleanup (); + if (groups == null && defaultGroup.IsEmpty () && operations.Count == 0 && idleConnections.Count == 0) { + Debug ($"MAIN LOOP DONE"); + running = 0; + idleSince = DateTime.UtcNow; + schedulerEvent.Reset (); + return; + } + + operationArray = new ValueTuple[operations.Count]; + operations.CopyTo (operationArray, 0); + idleArray = new ValueTuple[idleConnections.Count]; + idleConnections.CopyTo (idleArray, 0); + + taskList.Add (schedulerEvent.WaitAsync (maxIdleTime)); + foreach (var item in operationArray) + taskList.Add (item.Item2.WaitForCompletion (true)); + foreach (var item in idleArray) + taskList.Add (item.Item3); + } + + Debug ($"MAIN LOOP #1: operations={operationArray.Length} idle={idleArray.Length}"); + + var ret = await Task.WhenAny (taskList).ConfigureAwait (false); + + lock (ServicePoint) { + if (ret == taskList[0]) { + RunSchedulerIteration (); + continue; + } + + int idx = -1; + for (int i = 0; i < operationArray.Length; i++) { + if (ret == taskList[i + 1]) { + idx = i; + break; + } + } + + if (idx >= 0) { + var item = operationArray[idx]; + Debug ($"MAIN LOOP #2: {idx} group={item.Item1.ID} Op={item.Item2.ID}"); + operations.Remove (item); + + var opTask = (Task>)ret; + var runLoop = OperationCompleted (item.Item1, item.Item2, opTask); + Debug ($"MAIN LOOP #2 DONE: {idx} {runLoop}"); + if (runLoop) + RunSchedulerIteration (); + continue; + } + + for (int i = 0; i < idleArray.Length; i++) { + if (ret == taskList[i + 1 + operationArray.Length]) { + idx = i; + break; + } + } + + if (idx >= 0) { + var item = idleArray[idx]; + Debug ($"MAIN LOOP #3: {idx} group={item.Item1.ID} Cnc={item.Item2.ID}"); + idleConnections.Remove (item); + CloseIdleConnection (item.Item1, item.Item2); + } + } + } + } + + void Cleanup () + { + if (groups != null) { + var keys = new string[groups.Count]; + groups.Keys.CopyTo (keys, 0); + foreach (var groupName in keys) { + if (!groups.ContainsKey (groupName)) + continue; + var group = groups[groupName]; + if (group.IsEmpty ()) { + Debug ($"CLEANUP - REMOVING group={group.ID}"); + groups.Remove (groupName); + } + } + if (groups.Count == 0) + groups = null; + } + } + + void RunSchedulerIteration () + { + schedulerEvent.Reset (); + + bool repeat; + do { + Debug ($"ITERATION"); + + repeat = SchedulerIteration (defaultGroup); + + Debug ($"ITERATION #1: {repeat} {groups != null}"); + + if (groups != null) { + foreach (var group in groups) { + Debug ($"ITERATION #2: group={group.Value.ID}"); + repeat |= SchedulerIteration (group.Value); + } + } + + Debug ($"ITERATION #3: {repeat}"); + } while (repeat); + } + + bool OperationCompleted (ConnectionGroup group, WebOperation operation, Task<(bool, WebOperation)> task) + { +#if MONO_WEB_DEBUG + var me = $"{nameof (OperationCompleted)}(group={group.ID}, Op={operation.ID}, Cnc={operation.Connection.ID})"; +#else + string me = null; +#endif + + var (ok, next) = task.Status == TaskStatus.RanToCompletion ? task.Result : (false, null); + Debug ($"{me}: {task.Status} {ok} {next?.ID}"); + + if (!ok || !operation.Connection.Continue (next)) { + group.RemoveConnection (operation.Connection); + if (next == null) { + Debug ($"{me}: closed connection and done."); + return true; + } + ok = false; + } + + if (next == null) { + if (ok) { + var idleTask = Task.Delay (MaxIdleTime); + idleConnections.AddLast ((group, operation.Connection, idleTask)); + Debug ($"{me} keeping connection open for {MaxIdleTime} milliseconds."); + } else { + Debug ($"{me}: closed connection and done."); + } + return true; + } + + Debug ($"{me} got new operation next={next.ID}."); + operations.AddLast ((group, next)); + + if (ok) { + Debug ($"{me} continuing next={next.ID} on same connection."); + RemoveIdleConnection (operation.Connection); + return false; + } + + group.Cleanup (); + + var (connection, created) = group.CreateOrReuseConnection (next, true); + Debug ($"{me} created new connection Cnc={connection.ID} next={next.ID}."); + return false; + } + + void CloseIdleConnection (ConnectionGroup group, WebConnection connection) + { + Debug ($"{nameof (CloseIdleConnection)}(group={group.ID}, Cnc={connection.ID}) closing idle connection."); + + group.RemoveConnection (connection); + RemoveIdleConnection (connection); + } + + bool SchedulerIteration (ConnectionGroup group) + { +#if MONO_WEB_DEBUG + var me = $"{nameof (SchedulerIteration)}(group={group.ID})"; +#else + string me = null; +#endif + Debug ($"{me}"); + + // First, let's clean up. + group.Cleanup (); + + // Is there anything in the queue? + var next = group.GetNextOperation (); + Debug ($"{me} no pending operations."); + if (next == null) + return false; + + Debug ($"{me} found pending operation Op={next.ID}"); + + var (connection, created) = group.CreateOrReuseConnection (next, false); + if (connection == null) { + // All connections are currently busy, need to keep it in the queue for now. + Debug ($"{me} all connections busy, keeping operation in queue."); + return false; + } + + Debug ($"{me} started operation: Op={next.ID} Cnc={connection.ID}"); + operations.AddLast ((group, next)); + RemoveIdleConnection (connection); + return true; + } + + void RemoveOperation (WebOperation operation) + { + var iter = operations.First; + while (iter != null) { + var node = iter; + iter = iter.Next; + + if (node.Value.Item2 == operation) + operations.Remove (node); + } + } + + void RemoveIdleConnection (WebConnection connection) + { + var iter = idleConnections.First; + while (iter != null) { + var node = iter; + iter = iter.Next; + + if (node.Value.Item2 == connection) + idleConnections.Remove (node); + } + } + + public void SendRequest (WebOperation operation, string groupName) + { + lock (ServicePoint) { + var group = GetConnectionGroup (groupName); + Debug ($"SEND REQUEST: Op={operation.ID} group={group.ID}"); + group.EnqueueOperation (operation); + Run (); + Debug ($"SEND REQUEST DONE: Op={operation.ID} group={group.ID}"); + } + } + + public bool CloseConnectionGroup (string groupName) + { + lock (ServicePoint) { + ConnectionGroup group; + if (string.IsNullOrEmpty (groupName)) + group = defaultGroup; + else if (groups == null || !groups.TryGetValue (groupName, out group)) + return false; + + Debug ($"CLOSE CONNECTION GROUP: group={group.ID}"); + + if (group != defaultGroup) { + groups.Remove (groupName); + if (groups.Count == 0) + groups = null; + } + + group.Close (); + Run (); + return true; + } + } + + ConnectionGroup GetConnectionGroup (string name) + { + lock (ServicePoint) { + if (string.IsNullOrEmpty (name)) + return defaultGroup; + + if (groups == null) + groups = new Dictionary (); + + if (groups.TryGetValue (name, out ConnectionGroup group)) + return group; + + group = new ConnectionGroup (this, name); + groups.Add (name, group); + return group; + } + } + + void OnConnectionCreated (WebConnection connection) + { + Interlocked.Increment (ref currentConnections); + } + + void OnConnectionClosed (WebConnection connection) + { + RemoveIdleConnection (connection); + Interlocked.Decrement (ref currentConnections); + } + + class ConnectionGroup + { + public ServicePointScheduler Scheduler { + get; + } + + public string Name { + get; + } + + public bool IsDefault => string.IsNullOrEmpty (Name); + + static int nextId; + public readonly int ID = ++nextId; + LinkedList connections; + LinkedList queue; + + public ConnectionGroup (ServicePointScheduler scheduler, string name) + { + Scheduler = scheduler; + Name = name; + + connections = new LinkedList (); + queue = new LinkedList (); + } + + public bool IsEmpty () + { + return connections.Count == 0 && queue.Count == 0; + } + + public void RemoveConnection (WebConnection connection) + { + Scheduler.Debug ($"REMOVING CONNECTION: group={ID} cnc={connection.ID}"); + connections.Remove (connection); + connection.Dispose (); + Scheduler.OnConnectionClosed (connection); + } + + public void Cleanup () + { + var iter = connections.First; + while (iter != null) { + var connection = iter.Value; + var node = iter; + iter = iter.Next; + + if (connection.Closed) { + Scheduler.Debug ($"REMOVING CONNECTION: group={ID} cnc={connection.ID}"); + connections.Remove (node); + Scheduler.OnConnectionClosed (connection); + } + } + } + + public void Close () + { + foreach (var operation in queue) { + operation.Abort (); + Scheduler.RemoveOperation (operation); + } + queue.Clear (); + + foreach (var connection in connections) { + connection.Dispose (); + Scheduler.OnConnectionClosed (connection); + } + connections.Clear (); + } + + public void EnqueueOperation (WebOperation operation) + { + queue.AddLast (operation); + } + + public WebOperation GetNextOperation () + { + // Is there anything in the queue? + var iter = queue.First; + while (iter != null) { + var operation = iter.Value; + var node = iter; + iter = iter.Next; + + if (operation.Aborted) { + queue.Remove (node); + Scheduler.RemoveOperation (operation); + continue; + } + + return operation; + } + + return null; + } + + public WebConnection FindIdleConnection (WebOperation operation) + { + // First let's find the ideal candidate. + WebConnection candidate = null; + foreach (var connection in connections) { + if (connection.CanReuseConnection (operation)) { + if (candidate == null || connection.IdleSince > candidate.IdleSince) + candidate = connection; + } + } + + // Found one? Make sure it's actually willing to run it. + if (candidate != null && candidate.StartOperation (operation, true)) { + queue.Remove (operation); + return candidate; + } + + // Ok, let's loop again and pick the first one that accepts the new operation. + foreach (var connection in connections) { + if (connection.StartOperation (operation, true)) { + queue.Remove (operation); + return connection; + } + } + + return null; + } + + public (WebConnection connection, bool created) CreateOrReuseConnection (WebOperation operation, bool force) + { + var connection = FindIdleConnection (operation); + if (connection != null) + return (connection, false); + + if (force || Scheduler.ServicePoint.ConnectionLimit > connections.Count || connections.Count == 0) { + connection = new WebConnection (Scheduler.ServicePoint); + connection.StartOperation (operation, false); + connections.AddFirst (connection); + Scheduler.OnConnectionCreated (connection); + queue.Remove (operation); + return (connection, true); + } + + return (null, false); + } + } + + // https://blogs.msdn.microsoft.com/pfxteam/2012/02/11/building-async-coordination-primitives-part-1-asyncmanualresetevent/ + class AsyncManualResetEvent + { + volatile TaskCompletionSource m_tcs = new TaskCompletionSource (); + + public Task WaitAsync () { return m_tcs.Task; } + + public bool WaitOne (int millisecondTimeout) + { + WebConnection.Debug ($"AMRE WAIT ONE: {millisecondTimeout}"); + return m_tcs.Task.Wait (millisecondTimeout); + } + + public async Task WaitAsync (int millisecondTimeout) + { + var timeoutTask = Task.Delay (millisecondTimeout); + var ret = await Task.WhenAny (m_tcs.Task, timeoutTask).ConfigureAwait (false); + return ret != timeoutTask; + } + + public void Set () + { + var tcs = m_tcs; + Task.Factory.StartNew (s => ((TaskCompletionSource)s).TrySetResult (true), + tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default); + tcs.Task.Wait (); + } + + public void Reset () + { + while (true) { + var tcs = m_tcs; + if (!tcs.Task.IsCompleted || + Interlocked.CompareExchange (ref m_tcs, new TaskCompletionSource (), tcs) == tcs) + return; + } + } + + public AsyncManualResetEvent (bool state) + { + if (state) + Set (); + } + } + } +} diff --git a/mcs/class/System/System.Net/SimpleAsyncResult.cs b/mcs/class/System/System.Net/SimpleAsyncResult.cs deleted file mode 100644 index 6a2107c97735..000000000000 --- a/mcs/class/System/System.Net/SimpleAsyncResult.cs +++ /dev/null @@ -1,232 +0,0 @@ -// -// SimpleAsyncResult.cs -// -// Authors: -// Gonzalo Paniagua Javier (gonzalo@ximian.com) -// Martin Baulig (martin.baulig@xamarin.com) -// -// (C) 2003 Ximian, Inc (http://www.ximian.com) -// Copyright (c) 2014 Xamarin Inc. (http://www.xamarin.com) -// -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System.IO; -using System.Threading; - -namespace System.Net -{ - delegate void SimpleAsyncCallback (SimpleAsyncResult result); - - class SimpleAsyncResult : IAsyncResult - { - ManualResetEvent handle; - bool synch; - bool isCompleted; - readonly SimpleAsyncCallback cb; - object state; - bool callbackDone; - Exception exc; - object locker = new object (); - - SimpleAsyncResult (SimpleAsyncCallback cb) - { - this.cb = cb; - } - - protected SimpleAsyncResult (AsyncCallback cb, object state) - { - this.state = state; - this.cb = result => { - if (cb != null) - cb (this); - }; - } - - public static void Run (Func func, SimpleAsyncCallback callback) - { - var result = new SimpleAsyncResult (callback); - try { - if (!func (result)) - result.SetCompleted (true); - } catch (Exception ex) { - result.SetCompleted (true, ex); - } - } - - public static void RunWithLock (object locker, Func func, SimpleAsyncCallback callback) - { - Run (inner => { - bool running = func (inner); - if (running) - Monitor.Exit (locker); - return running; - }, inner => { - if (inner.GotException) { - if (inner.synch) - Monitor.Exit (locker); - callback (inner); - return; - } - - try { - if (!inner.synch) - Monitor.Enter (locker); - - callback (inner); - } finally { - Monitor.Exit (locker); - } - }); - } - - protected void Reset_internal () - { - callbackDone = false; - exc = null; - lock (locker) { - isCompleted = false; - if (handle != null) - handle.Reset (); - } - } - - internal void SetCompleted (bool synch, Exception e) - { - SetCompleted_internal (synch, e); - DoCallback_private (); - } - - internal void SetCompleted (bool synch) - { - SetCompleted_internal (synch); - DoCallback_private (); - } - - void SetCompleted_internal (bool synch, Exception e) - { - this.synch = synch; - exc = e; - lock (locker) { - isCompleted = true; - if (handle != null) - handle.Set (); - } - } - - protected void SetCompleted_internal (bool synch) - { - SetCompleted_internal (synch, null); - } - - void DoCallback_private () - { - if (callbackDone) - return; - callbackDone = true; - if (cb == null) - return; - cb (this); - } - - protected void DoCallback_internal () - { - if (!callbackDone && cb != null) { - callbackDone = true; - cb (this); - } - } - - internal void WaitUntilComplete () - { - if (IsCompleted) - return; - - AsyncWaitHandle.WaitOne (); - } - - internal bool WaitUntilComplete (int timeout, bool exitContext) - { - if (IsCompleted) - return true; - - return AsyncWaitHandle.WaitOne (timeout, exitContext); - } - - public object AsyncState { - get { return state; } - } - - public WaitHandle AsyncWaitHandle { - get { - lock (locker) { - if (handle == null) - handle = new ManualResetEvent (isCompleted); - } - - return handle; - } - } - - bool? user_read_synch; - - public bool CompletedSynchronously { - get { - // - // CompletedSynchronously (for System.Net networking stack) means "was the operation completed before the first time - // that somebody asked if it was completed synchronously"? They do this because some of their asynchronous operations - // (particularly those in the Socket class) will avoid the cost of capturing and transferring the ExecutionContext - // to the callback thread by checking CompletedSynchronously, and calling the callback from within BeginXxx instead of - // on the completion port thread if the native winsock call completes quickly. - // - // TODO: racy - if (user_read_synch != null) - return user_read_synch.Value; - - user_read_synch = synch; - return user_read_synch.Value; - } - } - - internal bool CompletedSynchronouslyPeek { - get { - return synch; - } - } - - public bool IsCompleted { - get { - lock (locker) { - return isCompleted; - } - } - } - - internal bool GotException { - get { return (exc != null); } - } - - internal Exception Exception { - get { return exc; } - } - } -} - diff --git a/mcs/class/System/System.Net/WebAsyncResult.cs b/mcs/class/System/System.Net/WebAsyncResult.cs deleted file mode 100644 index 52fc1a5967e9..000000000000 --- a/mcs/class/System/System.Net/WebAsyncResult.cs +++ /dev/null @@ -1,132 +0,0 @@ -// -// System.Net.WebAsyncResult -// -// Authors: -// Gonzalo Paniagua Javier (gonzalo@ximian.com) -// -// (C) 2003 Ximian, Inc (http://www.ximian.com) -// - -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System.IO; -using System.Threading; - -namespace System.Net -{ - class WebAsyncResult : SimpleAsyncResult - { - int nbytes; - IAsyncResult innerAsyncResult; - HttpWebResponse response; - Stream writeStream; - byte [] buffer; - int offset; - int size; - public bool EndCalled; - public bool AsyncWriteAll; - public HttpWebRequest AsyncObject; - - public WebAsyncResult (AsyncCallback cb, object state) - : base (cb, state) - { - } - - public WebAsyncResult (HttpWebRequest request, AsyncCallback cb, object state) - : base (cb, state) - { - this.AsyncObject = request; - } - - public WebAsyncResult (AsyncCallback cb, object state, byte [] buffer, int offset, int size) - : base (cb, state) - { - this.buffer = buffer; - this.offset = offset; - this.size = size; - } - - internal void Reset () - { - this.nbytes = 0; - this.response = null; - this.buffer = null; - this.offset = 0; - this.size = 0; - Reset_internal (); - } - - internal void SetCompleted (bool synch, int nbytes) - { - this.nbytes = nbytes; - SetCompleted_internal (synch); - } - - internal void SetCompleted (bool synch, Stream writeStream) - { - this.writeStream = writeStream; - SetCompleted_internal (synch); - } - - internal void SetCompleted (bool synch, HttpWebResponse response) - { - this.response = response; - SetCompleted_internal (synch); - } - - internal void DoCallback () - { - DoCallback_internal (); - } - - internal int NBytes { - get { return nbytes; } - set { nbytes = value; } - } - - internal IAsyncResult InnerAsyncResult { - get { return innerAsyncResult; } - set { innerAsyncResult = value; } - } - - internal Stream WriteStream { - get { return writeStream; } - } - - internal HttpWebResponse Response { - get { return response; } - } - - internal byte [] Buffer { - get { return buffer; } - } - - internal int Offset { - get { return offset; } - } - - internal int Size { - get { return size; } - } - } -} - diff --git a/mcs/class/System/System.Net/WebConnection.cs b/mcs/class/System/System.Net/WebConnection.cs index 68c480fa86eb..b545c39a9c6b 100644 --- a/mcs/class/System/System.Net/WebConnection.cs +++ b/mcs/class/System/System.Net/WebConnection.cs @@ -3,10 +3,11 @@ // // Authors: // Gonzalo Paniagua Javier (gonzalo@ximian.com) +// Martin Baulig // // (C) 2003 Ximian, Inc (http://www.ximian.com) +// Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com) // - // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the @@ -27,13 +28,14 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // - using System.IO; using System.Collections; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; +using System.Threading.Tasks; +using System.Runtime.ExceptionServices; using System.Diagnostics; using Mono.Net.Security; @@ -48,788 +50,245 @@ enum ReadState Aborted } - class WebConnection + class WebConnection : IDisposable { - ServicePoint sPoint; - Stream nstream; - internal Socket socket; - object socketLock = new object (); - IWebConnectionState state; - WebExceptionStatus status; - bool keepAlive; - byte [] buffer; - EventHandler abortHandler; - AbortHelper abortHelper; - internal WebConnectionData Data; - bool chunkedRead; - MonoChunkStream chunkStream; - Queue queue; - bool reused; - int position; - HttpWebRequest priority_request; NetworkCredential ntlm_credentials; bool ntlm_authenticated; bool unsafe_sharing; + Stream networkStream; + Socket socket; + MonoTlsStream monoTlsStream; + WebConnectionTunnel tunnel; + int disposed; - enum NtlmAuthState - { - None, - Challenge, - Response + public ServicePoint ServicePoint { + get; } - NtlmAuthState connect_ntlm_auth_state; - HttpWebRequest connect_request; - - Exception connect_exception; - MonoTlsStream tlsStream; #if MONOTOUCH && !MONOTOUCH_TV && !MONOTOUCH_WATCH [System.Runtime.InteropServices.DllImport ("__Internal")] static extern void xamarin_start_wwan (string uri); #endif - internal MonoChunkStream MonoChunkStream { - get { return chunkStream; } - } - - public WebConnection (IWebConnectionState wcs, ServicePoint sPoint) + public WebConnection (ServicePoint sPoint) { - this.state = wcs; - this.sPoint = sPoint; - buffer = new byte [4096]; - Data = new WebConnectionData (); - queue = wcs.Group.Queue; - abortHelper = new AbortHelper (); - abortHelper.Connection = this; - abortHandler = new EventHandler (abortHelper.Abort); + ServicePoint = sPoint; } - class AbortHelper { - public WebConnection Connection; - - public void Abort (object sender, EventArgs args) - { - WebConnection other = ((HttpWebRequest) sender).WebConnection; - if (other == null) - other = Connection; - other.Abort (sender, args); - } +#if MONO_WEB_DEBUG + internal static bool EnableWebDebug { + get; set; } - bool CanReuse () + static WebConnection () { - // The real condition is !(socket.Poll (0, SelectMode.SelectRead) || socket.Available != 0) - // but if there's data pending to read (!) we won't reuse the socket. - return (socket.Poll (0, SelectMode.SelectRead) == false); + if (Environment.GetEnvironmentVariable ("MONO_WEB_DEBUG") != null) + EnableWebDebug = true; } - - void Connect (HttpWebRequest request) - { - lock (socketLock) { - if (socket != null && socket.Connected && status == WebExceptionStatus.Success) { - // Take the chunked stream to the expected state (State.None) - if (CanReuse () && CompleteChunkedRead ()) { - reused = true; - return; - } - } - - reused = false; - if (socket != null) { - socket.Close(); - socket = null; - } - - chunkStream = null; - IPHostEntry hostEntry = sPoint.HostEntry; - - if (hostEntry == null) { -#if MONOTOUCH && !MONOTOUCH_TV && !MONOTOUCH_WATCH - xamarin_start_wwan (sPoint.Address.ToString ()); - hostEntry = sPoint.HostEntry; - if (hostEntry == null) { -#endif - status = sPoint.UsesProxy ? WebExceptionStatus.ProxyNameResolutionFailure : - WebExceptionStatus.NameResolutionFailure; - return; -#if MONOTOUCH && !MONOTOUCH_TV && !MONOTOUCH_WATCH - } #endif - } - - //WebConnectionData data = Data; - foreach (IPAddress address in hostEntry.AddressList) { - try { - socket = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - } catch (Exception se) { - // The Socket ctor can throw if we run out of FD's - if (!request.Aborted) - status = WebExceptionStatus.ConnectFailure; - connect_exception = se; - return; - } - IPEndPoint remote = new IPEndPoint (address, sPoint.Address.Port); - socket.NoDelay = !sPoint.UseNagleAlgorithm; - try { - sPoint.KeepAliveSetup (socket); - } catch { - // Ignore. Not supported in all platforms. - } - if (!sPoint.CallEndPointDelegate (socket, remote)) { - socket.Close (); - socket = null; - status = WebExceptionStatus.ConnectFailure; - } else { - try { - if (request.Aborted) - return; - socket.Connect (remote); - status = WebExceptionStatus.Success; - break; - } catch (ThreadAbortException) { - // program exiting... - Socket s = socket; - socket = null; - if (s != null) - s.Close (); - return; - } catch (ObjectDisposedException) { - // socket closed from another thread - return; - } catch (Exception exc) { - Socket s = socket; - socket = null; - if (s != null) - s.Close (); - if (!request.Aborted) - status = WebExceptionStatus.ConnectFailure; - connect_exception = exc; - } - } - } - } + [Conditional ("MONO_WEB_DEBUG")] + internal static void Debug (string message, params object[] args) + { +#if MONO_WEB_DEBUG + if (EnableWebDebug) + Console.Error.WriteLine (string.Format (message, args)); +#endif } - bool CreateTunnel (HttpWebRequest request, Uri connectUri, - Stream stream, out byte[] buffer) + [Conditional ("MONO_WEB_DEBUG")] + internal static void Debug (string message) { - StringBuilder sb = new StringBuilder (); - sb.Append ("CONNECT "); - sb.Append (request.Address.Host); - sb.Append (':'); - sb.Append (request.Address.Port); - sb.Append (" HTTP/"); - if (request.ServicePoint.ProtocolVersion == HttpVersion.Version11) - sb.Append ("1.1"); - else - sb.Append ("1.0"); - - sb.Append ("\r\nHost: "); - sb.Append (request.Address.Authority); - - bool ntlm = false; - var challenge = Data.Challenge; - Data.Challenge = null; - var auth_header = request.Headers ["Proxy-Authorization"]; - bool have_auth = auth_header != null; - if (have_auth) { - sb.Append ("\r\nProxy-Authorization: "); - sb.Append (auth_header); - ntlm = auth_header.ToUpper ().Contains ("NTLM"); - } else if (challenge != null && Data.StatusCode == 407) { - ICredentials creds = request.Proxy.Credentials; - have_auth = true; - - if (connect_request == null) { - // create a CONNECT request to use with Authenticate - connect_request = (HttpWebRequest)WebRequest.Create ( - connectUri.Scheme + "://" + connectUri.Host + ":" + connectUri.Port + "/"); - connect_request.Method = "CONNECT"; - connect_request.Credentials = creds; - } - - if (creds != null) { - for (int i = 0; i < challenge.Length; i++) { - var auth = AuthenticationManager.Authenticate (challenge [i], connect_request, creds); - if (auth == null) - continue; - ntlm = (auth.ModuleAuthenticationType == "NTLM"); - sb.Append ("\r\nProxy-Authorization: "); - sb.Append (auth.Message); - break; - } - } - } - - if (ntlm) { - sb.Append ("\r\nProxy-Connection: keep-alive"); - connect_ntlm_auth_state++; - } - - sb.Append ("\r\n\r\n"); - - Data.StatusCode = 0; - byte [] connectBytes = Encoding.Default.GetBytes (sb.ToString ()); - stream.Write (connectBytes, 0, connectBytes.Length); - - int status; - WebHeaderCollection result = ReadHeaders (stream, out buffer, out status); - if ((!have_auth || connect_ntlm_auth_state == NtlmAuthState.Challenge) && - result != null && status == 407) { // Needs proxy auth - var connectionHeader = result ["Connection"]; - if (socket != null && !string.IsNullOrEmpty (connectionHeader) && - connectionHeader.ToLower() == "close") { - // The server is requesting that this connection be closed - socket.Close(); - socket = null; - } - - Data.StatusCode = status; - Data.Challenge = result.GetValues ("Proxy-Authenticate"); - Data.Headers = result; - return false; - } - - if (status != 200) { - Data.StatusCode = status; - Data.Headers = result; - return false; - } - - return (result != null); +#if MONO_WEB_DEBUG + if (EnableWebDebug) + Console.Error.WriteLine (message); +#endif } - WebHeaderCollection ReadHeaders (Stream stream, out byte [] retBuffer, out int status) + bool CanReuse () { - retBuffer = null; - status = 200; - - byte [] buffer = new byte [1024]; - MemoryStream ms = new MemoryStream (); - - while (true) { - int n = stream.Read (buffer, 0, 1024); - if (n == 0) { - HandleError (WebExceptionStatus.ServerProtocolViolation, null, "ReadHeaders"); - return null; - } - - ms.Write (buffer, 0, n); - int start = 0; - string str = null; - bool gotStatus = false; - WebHeaderCollection headers = new WebHeaderCollection (); - while (ReadLine (ms.GetBuffer (), ref start, (int) ms.Length, ref str)) { - if (str == null) { - int contentLen = 0; - try { - contentLen = int.Parse(headers["Content-Length"]); - } - catch { - contentLen = 0; - } - - if (ms.Length - start - contentLen > 0) { - // we've read more data than the response header and conents, - // give back extra data to the caller - retBuffer = new byte[ms.Length - start - contentLen]; - Buffer.BlockCopy(ms.GetBuffer(), start + contentLen, retBuffer, 0, retBuffer.Length); - } - else { - // haven't read in some or all of the contents for the response, do so now - FlushContents(stream, contentLen - (int)(ms.Length - start)); - } - - return headers; - } - - if (gotStatus) { - headers.Add (str); - continue; - } - - string[] parts = str.Split (' '); - if (parts.Length < 2) { - HandleError (WebExceptionStatus.ServerProtocolViolation, null, "ReadHeaders2"); - return null; - } - - if (String.Compare (parts [0], "HTTP/1.1", true) == 0) - Data.ProxyVersion = HttpVersion.Version11; - else if (String.Compare (parts [0], "HTTP/1.0", true) == 0) - Data.ProxyVersion = HttpVersion.Version10; - else { - HandleError (WebExceptionStatus.ServerProtocolViolation, null, "ReadHeaders2"); - return null; - } - - status = (int)UInt32.Parse (parts [1]); - if (parts.Length >= 3) - Data.StatusDescription = String.Join (" ", parts, 2, parts.Length - 2); - - gotStatus = true; - } - } + // The real condition is !(socket.Poll (0, SelectMode.SelectRead) || socket.Available != 0) + // but if there's data pending to read (!) we won't reuse the socket. + return (socket.Poll (0, SelectMode.SelectRead) == false); } - void FlushContents(Stream stream, int contentLength) + bool CheckReusable () { - while (contentLength > 0) { - byte[] contentBuffer = new byte[contentLength]; - int bytesRead = stream.Read(contentBuffer, 0, contentLength); - if (bytesRead > 0) { - contentLength -= bytesRead; - } - else { - break; - } + if (socket != null && socket.Connected) { + try { + if (CanReuse ()) + return true; + } catch { } } + + return false; } - bool CreateStream (HttpWebRequest request) + async Task Connect (WebOperation operation, CancellationToken cancellationToken) { - try { - NetworkStream serverStream = new NetworkStream (socket, false); - - if (request.Address.Scheme == Uri.UriSchemeHttps) { -#if SECURITY_DEP - if (!reused || nstream == null || tlsStream == null) { - byte [] buffer = null; - if (sPoint.UseConnect) { - bool ok = CreateTunnel (request, sPoint.Address, serverStream, out buffer); - if (!ok) - return false; - } - tlsStream = new MonoTlsStream (request, serverStream); - nstream = tlsStream.CreateStream (buffer); + IPHostEntry hostEntry = ServicePoint.HostEntry; + + if (hostEntry == null || hostEntry.AddressList.Length == 0) { +#if MONOTOUCH && !MONOTOUCH_TV && !MONOTOUCH_WATCH + xamarin_start_wwan (ServicePoint.Address.ToString ()); + hostEntry = ServicePoint.HostEntry; + if (hostEntry == null) { +#endif + throw GetException (ServicePoint.UsesProxy ? WebExceptionStatus.ProxyNameResolutionFailure : + WebExceptionStatus.NameResolutionFailure, null); +#if MONOTOUCH && !MONOTOUCH_TV && !MONOTOUCH_WATCH } - // we also need to set ServicePoint.Certificate - // and ServicePoint.ClientCertificate but this can - // only be done later (after handshake - which is - // done only after a read operation). -#else - throw new NotSupportedException (); #endif - } else { - nstream = serverStream; - } - } catch (Exception ex) { - if (tlsStream != null) - status = tlsStream.ExceptionStatus; - else if (!request.Aborted) - status = WebExceptionStatus.ConnectFailure; - connect_exception = ex; - return false; } - return true; - } - - void HandleError (WebExceptionStatus st, Exception e, string where) - { - status = st; - lock (this) { - if (st == WebExceptionStatus.RequestCanceled) - Data = new WebConnectionData (); - } + foreach (IPAddress address in hostEntry.AddressList) { + operation.ThrowIfDisposed (cancellationToken); - if (e == null) { // At least we now where it comes from try { - throw new Exception (new System.Diagnostics.StackTrace ().ToString ()); - } catch (Exception e2) { - e = e2; + socket = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + } catch (Exception se) { + // The Socket ctor can throw if we run out of FD's + throw GetException (WebExceptionStatus.ConnectFailure, se); } - } - - HttpWebRequest req = null; - if (Data != null && Data.request != null) - req = Data.request; - - Close (true); - if (req != null) { - req.FinishedReading = true; - req.SetResponseError (st, e, where); - } - } - - void ReadDone (IAsyncResult result) - { - WebConnectionData data = Data; - Stream ns = nstream; - if (ns == null) { - Close (true); - return; - } - - int nread = -1; - try { - nread = ns.EndRead (result); - } catch (ObjectDisposedException) { - return; - } catch (Exception e) { - if (e.InnerException is ObjectDisposedException) - return; - - HandleError (WebExceptionStatus.ReceiveFailure, e, "ReadDone1"); - return; - } - - if (nread == 0) { - HandleError (WebExceptionStatus.ReceiveFailure, null, "ReadDone2"); - return; - } - - if (nread < 0) { - HandleError (WebExceptionStatus.ServerProtocolViolation, null, "ReadDone3"); - return; - } - - int pos = -1; - nread += position; - if (data.ReadState == ReadState.None) { - Exception exc = null; + IPEndPoint remote = new IPEndPoint (address, ServicePoint.Address.Port); + socket.NoDelay = !ServicePoint.UseNagleAlgorithm; try { - pos = GetResponse (data, sPoint, buffer, nread); - } catch (Exception e) { - exc = e; + ServicePoint.KeepAliveSetup (socket); + } catch { + // Ignore. Not supported in all platforms. } - if (exc != null || pos == -1) { - HandleError (WebExceptionStatus.ServerProtocolViolation, exc, "ReadDone4"); - return; + if (!ServicePoint.CallEndPointDelegate (socket, remote)) { + Interlocked.Exchange (ref socket, null)?.Close (); + continue; + } else { + try { + operation.ThrowIfDisposed (cancellationToken); + await socket.ConnectAsync (remote).ConfigureAwait (false); + } catch (ObjectDisposedException) { + throw; + } catch (Exception exc) { + Interlocked.Exchange (ref socket, null)?.Close (); + throw GetException (WebExceptionStatus.ConnectFailure, exc); + } } - } - - if (data.ReadState == ReadState.Aborted) { - HandleError (WebExceptionStatus.RequestCanceled, null, "ReadDone"); - return; - } - - if (data.ReadState != ReadState.Content) { - int est = nread * 2; - int max = (est < buffer.Length) ? buffer.Length : est; - byte [] newBuffer = new byte [max]; - Buffer.BlockCopy (buffer, 0, newBuffer, 0, nread); - buffer = newBuffer; - position = nread; - data.ReadState = ReadState.None; - InitRead (); - return; - } - - position = 0; - - WebConnectionStream stream = new WebConnectionStream (this, data); - bool expect_content = ExpectContent (data.StatusCode, data.request.Method); - string tencoding = null; - if (expect_content) - tencoding = data.Headers ["Transfer-Encoding"]; - chunkedRead = (tencoding != null && tencoding.IndexOf ("chunked", StringComparison.OrdinalIgnoreCase) != -1); - if (!chunkedRead) { - stream.ReadBuffer = buffer; - stream.ReadBufferOffset = pos; - stream.ReadBufferSize = nread; - try { - stream.CheckResponseInBuffer (); - } catch (Exception e) { - HandleError (WebExceptionStatus.ReceiveFailure, e, "ReadDone7"); - } - } else if (chunkStream == null) { - try { - chunkStream = new MonoChunkStream (buffer, pos, nread, data.Headers); - } catch (Exception e) { - HandleError (WebExceptionStatus.ServerProtocolViolation, e, "ReadDone5"); - return; - } - } else { - chunkStream.ResetBuffer (); - try { - chunkStream.Write (buffer, pos, nread); - } catch (Exception e) { - HandleError (WebExceptionStatus.ServerProtocolViolation, e, "ReadDone6"); + if (socket != null) return; - } } - data.stream = stream; - - if (!expect_content) - stream.ForceCompletion (); - - data.request.SetResponseData (data); + throw GetException (WebExceptionStatus.ConnectFailure, null); } - static bool ExpectContent (int statusCode, string method) - { - if (method == "HEAD") - return false; - return (statusCode >= 200 && statusCode != 204 && statusCode != 304); - } +#if MONO_WEB_DEBUG + static int nextID, nextRequestID; + readonly int id = ++nextID; + public int ID => disposed != 0 ? -id : id; +#else + internal readonly int ID; +#endif - internal void InitRead () + async Task CreateStream (WebOperation operation, bool reused, CancellationToken cancellationToken) { - Stream ns = nstream; +#if MONO_WEB_DEBUG + var requestID = ++nextRequestID; +#else + var requestID = 0; +#endif try { - int size = buffer.Length - position; - ns.BeginRead (buffer, position, size, ReadDone, null); - } catch (Exception e) { - HandleError (WebExceptionStatus.ReceiveFailure, e, "InitRead"); - } - } - - static int GetResponse (WebConnectionData data, ServicePoint sPoint, - byte [] buffer, int max) - { - int pos = 0; - string line = null; - bool lineok = false; - bool isContinue = false; - bool emptyFirstLine = false; - do { - if (data.ReadState == ReadState.Aborted) - return -1; - - if (data.ReadState == ReadState.None) { - lineok = ReadLine (buffer, ref pos, max, ref line); - if (!lineok) - return 0; - - if (line == null) { - emptyFirstLine = true; - continue; - } - emptyFirstLine = false; - data.ReadState = ReadState.Status; - - string [] parts = line.Split (' '); - if (parts.Length < 2) - return -1; - - if (String.Compare (parts [0], "HTTP/1.1", true) == 0) { - data.Version = HttpVersion.Version11; - sPoint.SetVersion (HttpVersion.Version11); - } else { - data.Version = HttpVersion.Version10; - sPoint.SetVersion (HttpVersion.Version10); - } - - data.StatusCode = (int) UInt32.Parse (parts [1]); - if (parts.Length >= 3) - data.StatusDescription = String.Join (" ", parts, 2, parts.Length - 2); - else - data.StatusDescription = ""; - - if (pos >= max) - return pos; - } - - emptyFirstLine = false; - if (data.ReadState == ReadState.Status) { - data.ReadState = ReadState.Headers; - data.Headers = new WebHeaderCollection (); - ArrayList headers = new ArrayList (); - bool finished = false; - while (!finished) { - if (ReadLine (buffer, ref pos, max, ref line) == false) - break; - - if (line == null) { - // Empty line: end of headers - finished = true; - continue; - } - - if (line.Length > 0 && (line [0] == ' ' || line [0] == '\t')) { - int count = headers.Count - 1; - if (count < 0) - break; - - string prev = (string) headers [count] + line; - headers [count] = prev; - } else { - headers.Add (line); - } - } - - if (!finished) - return 0; - - // .NET uses ParseHeaders or ParseHeadersStrict which is much better - foreach (string s in headers) { + var stream = new NetworkStream (socket, false); - int pos_s = s.IndexOf (':'); - if (pos_s == -1) - throw new ArgumentException ("no colon found", "header"); + Debug ($"WC CREATE STREAM: Cnc={ID} {requestID} {reused} socket={socket.ID}"); - var header = s.Substring (0, pos_s); - var value = s.Substring (pos_s + 1).Trim (); - - var h = data.Headers; - if (WebHeaderCollection.AllowMultiValues (header)) { - h.AddInternal (header, value); - } else { - h.SetInternal (header, value); - } - } - - if (data.StatusCode == (int) HttpStatusCode.Continue) { - sPoint.SendContinue = true; - if (pos >= max) - return pos; - - if (data.request.ExpectContinue) { - data.request.DoContinueDelegate (data.StatusCode, data.Headers); - // Prevent double calls when getting the - // headers in several packets. - data.request.ExpectContinue = false; + if (operation.Request.Address.Scheme == Uri.UriSchemeHttps) { + if (!reused || monoTlsStream == null) { + if (ServicePoint.UseConnect) { + if (tunnel == null) + tunnel = new WebConnectionTunnel (operation.Request, ServicePoint.Address); + await tunnel.Initialize (stream, cancellationToken).ConfigureAwait (false); + if (!tunnel.Success) + return false; } - - data.ReadState = ReadState.None; - isContinue = true; - } - else { - data.ReadState = ReadState.Content; - return pos; + monoTlsStream = new MonoTlsStream (operation.Request, stream); + networkStream = await monoTlsStream.CreateStream (tunnel, cancellationToken).ConfigureAwait (false); } + return true; } - } while (emptyFirstLine || isContinue); - return -1; + networkStream = stream; + return true; + } catch (Exception ex) { + ex = HttpWebRequest.FlattenException (ex); + Debug ($"WC CREATE STREAM EX: Cnc={ID} {requestID} {operation.Aborted} - {ex.Message}"); + if (operation.Aborted || monoTlsStream == null) + throw GetException (WebExceptionStatus.ConnectFailure, ex); + throw GetException (monoTlsStream.ExceptionStatus, ex); + } finally { + Debug ($"WC CREATE STREAM DONE: Cnc={ID} {requestID}"); + } } - - void InitConnection (HttpWebRequest request) - { - request.WebConnection = this; - if (request.ReuseConnection) - request.StoredConnection = this; - if (request.Aborted) - return; + internal async Task InitConnection (WebOperation operation, CancellationToken cancellationToken) + { + Debug ($"WC INIT CONNECTION: Cnc={ID} Req={operation.Request.ID} Op={operation.ID}"); - keepAlive = request.KeepAlive; - Data = new WebConnectionData (request); + bool reset = true; retry: - Connect (request); - if (request.Aborted) - return; - - if (status != WebExceptionStatus.Success) { - if (!request.Aborted) { - request.SetWriteStreamError (status, connect_exception); - Close (true); + operation.ThrowIfClosedOrDisposed (cancellationToken); + + var reused = CheckReusable (); + Debug ($"WC INIT CONNECTION #1: Cnc={ID} Op={operation.ID} - {reused} - {operation.WriteBuffer != null} {operation.IsNtlmChallenge}"); + if (!reused) { + CloseSocket (); + if (reset) + Reset (); + try { + await Connect (operation, cancellationToken).ConfigureAwait (false); + Debug ($"WC INIT CONNECTION #2: Cnc={ID} Op={operation.ID} {socket.LocalEndPoint}"); + } catch (Exception ex) { + Debug ($"WC INIT CONNECTION #2 FAILED: Cnc={ID} Op={operation.ID} - {ex.Message}\n{ex}"); + throw; } - return; } - - if (!CreateStream (request)) { - if (request.Aborted) - return; - WebExceptionStatus st = status; - if (Data.Challenge != null) - goto retry; + var success = await CreateStream (operation, reused, cancellationToken).ConfigureAwait (false); - Exception cnc_exc = connect_exception; - if (cnc_exc == null && (Data.StatusCode == 401 || Data.StatusCode == 407)) { - st = WebExceptionStatus.ProtocolError; - if (Data.Headers == null) - Data.Headers = new WebHeaderCollection (); + Debug ($"WC INIT CONNECTION #3: Cnc={ID} Op={operation.ID} - {success}"); + if (!success) { + if (tunnel?.Challenge == null) + throw GetException (WebExceptionStatus.ProtocolError, null); - var webResponse = new HttpWebResponse (sPoint.Address, "CONNECT", Data, null); - cnc_exc = new WebException (Data.StatusCode == 407 ? "(407) Proxy Authentication Required" : "(401) Unauthorized", null, st, webResponse); - } - - connect_exception = null; - request.SetWriteStreamError (st, cnc_exc); - Close (true); - return; + if (tunnel.CloseConnection) + CloseSocket (); + reset = false; + goto retry; } - request.SetWriteStream (new WebConnectionStream (this, request)); + return new WebRequestStream (this, operation, networkStream, tunnel); } -#if MONOTOUCH - static bool warned_about_queue = false; -#endif - - internal EventHandler SendRequest (HttpWebRequest request) - { - if (request.Aborted) - return null; - - lock (this) { - if (state.TrySetBusy ()) { - status = WebExceptionStatus.Success; - ThreadPool.QueueUserWorkItem (o => { try { InitConnection ((HttpWebRequest) o); } catch {} }, request); - } else { - lock (queue) { -#if MONOTOUCH - if (!warned_about_queue) { - warned_about_queue = true; - Console.WriteLine ("WARNING: An HttpWebRequest was added to the ConnectionGroup queue because the connection limit was reached."); - } -#endif - queue.Enqueue (request); - } - } - } - - return abortHandler; - } - - void SendNext () + internal static WebException GetException (WebExceptionStatus status, Exception error) { - lock (queue) { - if (queue.Count > 0) { - SendRequest ((HttpWebRequest) queue.Dequeue ()); - } - } + if (error == null) + return new WebException ($"Error: {status}", status); + if (error is WebException wex) + return wex; + return new WebException ($"Error: {status} ({error.Message})", status, + WebExceptionInternalStatus.RequestFatal, error); } - internal void NextRead () - { - lock (this) { - if (Data.request != null) - Data.request.FinishedReading = true; - string header = (sPoint.UsesProxy) ? "Proxy-Connection" : "Connection"; - string cncHeader = (Data.Headers != null) ? Data.Headers [header] : null; - bool keepAlive = (Data.Version == HttpVersion.Version11 && this.keepAlive); - if (Data.ProxyVersion != null && Data.ProxyVersion != HttpVersion.Version11) - keepAlive = false; - if (cncHeader != null) { - cncHeader = cncHeader.ToLower (); - keepAlive = (this.keepAlive && cncHeader.IndexOf ("keep-alive", StringComparison.Ordinal) != -1); - } - - if ((socket != null && !socket.Connected) || - (!keepAlive || (cncHeader != null && cncHeader.IndexOf ("close", StringComparison.Ordinal) != -1))) { - Close (false); - } - - state.SetIdle (); - if (priority_request != null) { - SendRequest (priority_request); - priority_request = null; - } else { - SendNext (); - } - } - } - - static bool ReadLine (byte [] buffer, ref int start, int max, ref string output) + internal static bool ReadLine (byte[] buffer, ref int start, int max, ref string output) { bool foundCR = false; StringBuilder text = new StringBuilder (); int c = 0; while (start < max) { - c = (int) buffer [start++]; + c = (int)buffer[start++]; - if (c == '\n') { // newline - if ((text.Length > 0) && (text [text.Length - 1] == '\r')) + if (c == '\n') { // newline + if ((text.Length > 0) && (text[text.Length - 1] == '\r')) text.Length--; foundCR = false; @@ -841,9 +300,9 @@ static bool ReadLine (byte [] buffer, ref int start, int max, ref string output) if (c == '\r') foundCR = true; - - text.Append ((char) c); + + text.Append ((char)c); } if (c != '\n' && c != '\r') @@ -861,357 +320,180 @@ static bool ReadLine (byte [] buffer, ref int start, int max, ref string output) return true; } - - internal IAsyncResult BeginRead (HttpWebRequest request, byte [] buffer, int offset, int size, AsyncCallback cb, object state) + internal bool CanReuseConnection (WebOperation operation) { - Stream s = null; lock (this) { - if (Data.request != request) - throw new ObjectDisposedException (typeof (NetworkStream).FullName); - if (nstream == null) - return null; - s = nstream; - } - - IAsyncResult result = null; - if (!chunkedRead || (!chunkStream.DataAvailable && chunkStream.WantMore)) { - try { - result = s.BeginRead (buffer, offset, size, cb, state); - cb = null; - } catch (Exception) { - HandleError (WebExceptionStatus.ReceiveFailure, null, "chunked BeginRead"); - throw; - } - } - - if (chunkedRead) { - WebAsyncResult wr = new WebAsyncResult (cb, state, buffer, offset, size); - wr.InnerAsyncResult = result; - if (result == null) { - // Will be completed from the data in MonoChunkStream - wr.SetCompleted (true, (Exception) null); - wr.DoCallback (); - } - return wr; - } + if (Closed || currentOperation != null) + return false; + if (!NtlmAuthenticated) + return true; - return result; - } - - internal int EndRead (HttpWebRequest request, IAsyncResult result) - { - Stream s = null; - lock (this) { - if (request.Aborted) - throw new WebException ("Request aborted", WebExceptionStatus.RequestCanceled); - if (Data.request != request) - throw new ObjectDisposedException (typeof (NetworkStream).FullName); - if (nstream == null) - throw new ObjectDisposedException (typeof (NetworkStream).FullName); - s = nstream; - } + NetworkCredential cnc_cred = NtlmCredential; + var request = operation.Request; - int nbytes = 0; - bool done = false; - WebAsyncResult wr = null; - IAsyncResult nsAsync = ((WebAsyncResult) result).InnerAsyncResult; - if (chunkedRead && (nsAsync is WebAsyncResult)) { - wr = (WebAsyncResult) nsAsync; - IAsyncResult inner = wr.InnerAsyncResult; - if (inner != null && !(inner is WebAsyncResult)) { - nbytes = s.EndRead (inner); - done = nbytes == 0; - } - } else if (!(nsAsync is WebAsyncResult)) { - nbytes = s.EndRead (nsAsync); - wr = (WebAsyncResult) result; - done = nbytes == 0; - } + bool isProxy = (request.Proxy != null && !request.Proxy.IsBypassed (request.RequestUri)); + ICredentials req_icreds = (!isProxy) ? request.Credentials : request.Proxy.Credentials; + NetworkCredential req_cred = (req_icreds != null) ? req_icreds.GetCredential (request.RequestUri, "NTLM") : null; - if (chunkedRead) { - try { - chunkStream.WriteAndReadBack (wr.Buffer, wr.Offset, wr.Size, ref nbytes); - if (!done && nbytes == 0 && chunkStream.WantMore) - nbytes = EnsureRead (wr.Buffer, wr.Offset, wr.Size); - } catch (Exception e) { - if (e is WebException) - throw e; - - throw new WebException ("Invalid chunked data.", e, - WebExceptionStatus.ServerProtocolViolation, null); - } - - if ((done || nbytes == 0) && chunkStream.ChunkLeft != 0) { - HandleError (WebExceptionStatus.ReceiveFailure, null, "chunked EndRead"); - throw new WebException ("Read error", null, WebExceptionStatus.ReceiveFailure, null); + if (cnc_cred == null || req_cred == null || + cnc_cred.Domain != req_cred.Domain || cnc_cred.UserName != req_cred.UserName || + cnc_cred.Password != req_cred.Password) { + return false; } - } - - return (nbytes != 0) ? nbytes : -1; - } - // To be called on chunkedRead when we can read no data from the MonoChunkStream yet - int EnsureRead (byte [] buffer, int offset, int size) - { - byte [] morebytes = null; - int nbytes = 0; - while (nbytes == 0 && chunkStream.WantMore) { - int localsize = chunkStream.ChunkLeft; - if (localsize <= 0) // not read chunk size yet - localsize = 1024; - else if (localsize > 16384) - localsize = 16384; - - if (morebytes == null || morebytes.Length < localsize) - morebytes = new byte [localsize]; - - int nread = nstream.Read (morebytes, 0, localsize); - if (nread <= 0) - return 0; // Error - - chunkStream.Write (morebytes, 0, nread); - nbytes += chunkStream.Read (buffer, offset + nbytes, size - nbytes); + bool req_sharing = request.UnsafeAuthenticatedConnectionSharing; + bool cnc_sharing = UnsafeAuthenticatedConnectionSharing; + return !(req_sharing == false || req_sharing != cnc_sharing); } - - return nbytes; } - bool CompleteChunkedRead() + bool PrepareSharingNtlm (WebOperation operation) { - if (!chunkedRead || chunkStream == null) + if (operation == null || !NtlmAuthenticated) return true; - while (chunkStream.WantMore) { - int nbytes = nstream.Read (buffer, 0, buffer.Length); - if (nbytes <= 0) - return false; // Socket was disconnected + bool needs_reset = false; + NetworkCredential cnc_cred = NtlmCredential; + var request = operation.Request; - chunkStream.Write (buffer, 0, nbytes); - } - - return true; - } + bool isProxy = (request.Proxy != null && !request.Proxy.IsBypassed (request.RequestUri)); + ICredentials req_icreds = (!isProxy) ? request.Credentials : request.Proxy.Credentials; + NetworkCredential req_cred = (req_icreds != null) ? req_icreds.GetCredential (request.RequestUri, "NTLM") : null; - internal IAsyncResult BeginWrite (HttpWebRequest request, byte [] buffer, int offset, int size, AsyncCallback cb, object state) - { - Stream s = null; - lock (this) { - if (Data.request != request) - throw new ObjectDisposedException (typeof (NetworkStream).FullName); - if (nstream == null) - return null; - s = nstream; + if (cnc_cred == null || req_cred == null || + cnc_cred.Domain != req_cred.Domain || cnc_cred.UserName != req_cred.UserName || + cnc_cred.Password != req_cred.Password) { + needs_reset = true; } - IAsyncResult result = null; - try { - result = s.BeginWrite (buffer, offset, size, cb, state); - } catch (ObjectDisposedException) { - lock (this) { - if (Data.request != request) - return null; - } - throw; - } catch (IOException e) { - SocketException se = e.InnerException as SocketException; - if (se != null && se.SocketErrorCode == SocketError.NotConnected) { - return null; - } - throw; - } catch (Exception) { - status = WebExceptionStatus.SendFailure; - throw; + if (!needs_reset) { + bool req_sharing = request.UnsafeAuthenticatedConnectionSharing; + bool cnc_sharing = UnsafeAuthenticatedConnectionSharing; + needs_reset = (req_sharing == false || req_sharing != cnc_sharing); } - return result; + return needs_reset; } - internal bool EndWrite (HttpWebRequest request, bool throwOnError, IAsyncResult result) + void Reset () { - Stream s = null; lock (this) { - if (status == WebExceptionStatus.RequestCanceled) - return true; - if (Data.request != request) - throw new ObjectDisposedException (typeof (NetworkStream).FullName); - if (nstream == null) - throw new ObjectDisposedException (typeof (NetworkStream).FullName); - s = nstream; - } - - try { - s.EndWrite (result); - return true; - } catch (Exception exc) { - status = WebExceptionStatus.SendFailure; - if (throwOnError && exc.InnerException != null) - throw exc.InnerException; - return false; + tunnel = null; + ResetNtlm (); } } - internal int Read (HttpWebRequest request, byte [] buffer, int offset, int size) + void Close (bool reset) { - Stream s = null; lock (this) { - if (Data.request != request) - throw new ObjectDisposedException (typeof (NetworkStream).FullName); - if (nstream == null) - return 0; - s = nstream; + CloseSocket (); + if (reset) + Reset (); } + } - int result = 0; - try { - bool done = false; - if (!chunkedRead) { - result = s.Read (buffer, offset, size); - done = (result == 0); + void CloseSocket () + { + lock (this) { + if (networkStream != null) { + try { + networkStream.Dispose (); + } catch { } + networkStream = null; } - if (chunkedRead) { + if (socket != null) { try { - chunkStream.WriteAndReadBack (buffer, offset, size, ref result); - if (!done && result == 0 && chunkStream.WantMore) - result = EnsureRead (buffer, offset, size); - } catch (Exception e) { - HandleError (WebExceptionStatus.ReceiveFailure, e, "chunked Read1"); - throw; - } - - if ((done || result == 0) && chunkStream.WantMore) { - HandleError (WebExceptionStatus.ReceiveFailure, null, "chunked Read2"); - throw new WebException ("Read error", null, WebExceptionStatus.ReceiveFailure, null); - } + socket.Dispose (); + } catch { } + socket = null; } - } catch (Exception e) { - HandleError (WebExceptionStatus.ReceiveFailure, e, "Read"); + + monoTlsStream = null; } + } + + DateTime idleSince; + WebOperation currentOperation; - return result; + public bool Closed => disposed != 0; + + public bool Busy { + get { return currentOperation != null; } + } + + public DateTime IdleSince { + get { return idleSince; } } - internal bool Write (HttpWebRequest request, byte [] buffer, int offset, int size, ref string err_msg) + public bool StartOperation (WebOperation operation, bool reused) { - err_msg = null; - Stream s = null; lock (this) { - if (Data.request != request) - throw new ObjectDisposedException (typeof (NetworkStream).FullName); - s = nstream; - if (s == null) + if (Closed) return false; - } - - try { - s.Write (buffer, offset, size); - } catch (Exception e) { - err_msg = e.Message; - WebExceptionStatus wes = WebExceptionStatus.SendFailure; - string msg = "Write: " + err_msg; - if (e is WebException) { - HandleError (wes, e, msg); + if (Interlocked.CompareExchange (ref currentOperation, operation, null) != null) return false; + + idleSince = DateTime.UtcNow + TimeSpan.FromDays (3650); + + if (reused && !PrepareSharingNtlm (operation)) { + Debug ($"WC START - CAN'T REUSE: Cnc={ID} Op={operation.ID}"); + Close (true); } - HandleError (wes, e, msg); - return false; + operation.RegisterRequest (ServicePoint, this); + Debug ($"WC START: Cnc={ID} Op={operation.ID}"); } + + operation.Run (); return true; } - internal void Close (bool sendNext) + public bool Continue (WebOperation next) { lock (this) { - if (Data != null && Data.request != null && Data.request.ReuseConnection) { - Data.request.ReuseConnection = false; - return; - } + if (Closed) + return false; - if (nstream != null) { - try { - nstream.Close (); - } catch {} - nstream = null; + Debug ($"WC CONTINUE: Cnc={ID} connected={socket?.Connected} next={next?.ID} current={currentOperation?.ID}"); + if (socket == null || !socket.Connected || !PrepareSharingNtlm (next)) { + Close (true); + return false; } - if (socket != null) { - try { - socket.Close (); - } catch {} - socket = null; - } + currentOperation = next; - if (ntlm_authenticated) - ResetNtlm (); - if (Data != null) { - lock (Data) { - Data.ReadState = ReadState.Aborted; - } - } - state.SetIdle (); - Data = new WebConnectionData (); - if (sendNext) - SendNext (); - - connect_request = null; - connect_ntlm_auth_state = NtlmAuthState.None; + if (next == null) + return true; + + // Ok, we got another connection. Let's run it! + next.RegisterRequest (ServicePoint, this); } + + next.Run (); + return true; } - void Abort (object sender, EventArgs args) + void Dispose (bool disposing) { - lock (this) { - lock (queue) { - HttpWebRequest req = (HttpWebRequest) sender; - if (Data.request == req || Data.request == null) { - if (!req.FinishedReading) { - status = WebExceptionStatus.RequestCanceled; - Close (false); - if (queue.Count > 0) { - Data.request = (HttpWebRequest) queue.Dequeue (); - SendRequest (Data.request); - } - } - return; - } + if (Interlocked.CompareExchange (ref disposed, 1, 0) != 0) + return; + Debug ($"WC DISPOSE: Cnc={ID}"); + Close (true); + } - req.FinishedReading = true; - req.SetResponseError (WebExceptionStatus.RequestCanceled, null, "User aborted"); - if (queue.Count > 0 && queue.Peek () == sender) { - queue.Dequeue (); - } else if (queue.Count > 0) { - object [] old = queue.ToArray (); - queue.Clear (); - for (int i = old.Length - 1; i >= 0; i--) { - if (old [i] != sender) - queue.Enqueue (old [i]); - } - } - } - } + public void Dispose () + { + Dispose (true); } - internal void ResetNtlm () + void ResetNtlm () { ntlm_authenticated = false; ntlm_credentials = null; unsafe_sharing = false; } - internal bool Connected { - get { - lock (this) { - return (socket != null && socket.Connected); - } - } - } - - // -Used for NTLM authentication - internal HttpWebRequest PriorityRequest { - set { priority_request = value; } - } - internal bool NtlmAuthenticated { get { return ntlm_authenticated; } set { ntlm_authenticated = value; } diff --git a/mcs/class/System/System.Net/WebConnectionData.cs b/mcs/class/System/System.Net/WebConnectionData.cs deleted file mode 100644 index 77d19540be16..000000000000 --- a/mcs/class/System/System.Net/WebConnectionData.cs +++ /dev/null @@ -1,80 +0,0 @@ -// -// System.Net.WebConnectionData -// -// Authors: -// Gonzalo Paniagua Javier (gonzalo@ximian.com) -// -// (C) 2003 Ximian, Inc (http://www.ximian.com) -// - -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System.IO; - -namespace System.Net -{ - class WebConnectionData - { - HttpWebRequest _request; - public int StatusCode; - public string StatusDescription; - public WebHeaderCollection Headers; - public Version Version; - public Version ProxyVersion; - public Stream stream; - public string[] Challenge; - ReadState _readState; - - public WebConnectionData () - { - _readState = ReadState.None; - } - - public WebConnectionData (HttpWebRequest request) - { - this._request = request; - } - - public HttpWebRequest request { - get { - return _request; - } - set { - _request = value; - } - } - - public ReadState ReadState { - get { - return _readState; - } - set { - lock (this) { - if ((_readState == ReadState.Aborted) && (value != ReadState.Aborted)) - throw new WebException ("Aborted", WebExceptionStatus.RequestCanceled); - _readState = value; - } - } - } - } -} - diff --git a/mcs/class/System/System.Net/WebConnectionGroup.cs b/mcs/class/System/System.Net/WebConnectionGroup.cs deleted file mode 100644 index 3936ee428981..000000000000 --- a/mcs/class/System/System.Net/WebConnectionGroup.cs +++ /dev/null @@ -1,292 +0,0 @@ -// -// System.Net.WebConnectionGroup -// -// Authors: -// Gonzalo Paniagua Javier (gonzalo@ximian.com) -// Martin Baulig (martin.baulig@xamarin.com) -// -// (C) 2003 Ximian, Inc (http://www.ximian.com) -// Copyright 2011-2014 Xamarin, Inc (http://www.xamarin.com) -// - -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Threading; -using System.Collections; -using System.Collections.Generic; -using System.Net.Configuration; -using System.Net.Sockets; -using System.Diagnostics; - -namespace System.Net -{ - class WebConnectionGroup - { - ServicePoint sPoint; - string name; - LinkedList connections; - Queue queue; - bool closing; - - public WebConnectionGroup (ServicePoint sPoint, string name) - { - this.sPoint = sPoint; - this.name = name; - connections = new LinkedList (); - queue = new Queue (); - } - - public event EventHandler ConnectionClosed; - - void OnConnectionClosed () - { - if (ConnectionClosed != null) - ConnectionClosed (this, null); - } - - public void Close () - { - List connectionsToClose = null; - - //TODO: what do we do with the queue? Empty it out and abort the requests? - //TODO: abort requests or wait for them to finish - lock (sPoint) { - closing = true; - var iter = connections.First; - while (iter != null) { - var cnc = iter.Value.Connection; - var node = iter; - iter = iter.Next; - - // Closing connections inside the lock leads to a deadlock. - if (connectionsToClose == null) - connectionsToClose = new List(); - - connectionsToClose.Add (cnc); - connections.Remove (node); - } - } - - if (connectionsToClose != null) { - foreach (var cnc in connectionsToClose) { - cnc.Close (false); - OnConnectionClosed (); - } - } - } - - public WebConnection GetConnection (HttpWebRequest request, out bool created) - { - lock (sPoint) { - return CreateOrReuseConnection (request, out created); - } - } - - static void PrepareSharingNtlm (WebConnection cnc, HttpWebRequest request) - { - if (!cnc.NtlmAuthenticated) - return; - - bool needs_reset = false; - NetworkCredential cnc_cred = cnc.NtlmCredential; - - bool isProxy = (request.Proxy != null && !request.Proxy.IsBypassed (request.RequestUri)); - ICredentials req_icreds = (!isProxy) ? request.Credentials : request.Proxy.Credentials; - NetworkCredential req_cred = (req_icreds != null) ? req_icreds.GetCredential (request.RequestUri, "NTLM") : null; - - if (cnc_cred == null || req_cred == null || - cnc_cred.Domain != req_cred.Domain || cnc_cred.UserName != req_cred.UserName || - cnc_cred.Password != req_cred.Password) { - needs_reset = true; - } - - if (!needs_reset) { - bool req_sharing = request.UnsafeAuthenticatedConnectionSharing; - bool cnc_sharing = cnc.UnsafeAuthenticatedConnectionSharing; - needs_reset = (req_sharing == false || req_sharing != cnc_sharing); - } - if (needs_reset) { - cnc.Close (false); // closes the authenticated connection - cnc.ResetNtlm (); - } - } - - ConnectionState FindIdleConnection () - { - foreach (var cnc in connections) { - if (cnc.Busy) - continue; - - connections.Remove (cnc); - connections.AddFirst (cnc); - return cnc; - } - - return null; - } - - WebConnection CreateOrReuseConnection (HttpWebRequest request, out bool created) - { - var cnc = FindIdleConnection (); - if (cnc != null) { - created = false; - PrepareSharingNtlm (cnc.Connection, request); - return cnc.Connection; - } - - if (sPoint.ConnectionLimit > connections.Count || connections.Count == 0) { - created = true; - cnc = new ConnectionState (this); - connections.AddFirst (cnc); - return cnc.Connection; - } - - created = false; - cnc = connections.Last.Value; - connections.Remove (cnc); - connections.AddFirst (cnc); - return cnc.Connection; - } - - public string Name { - get { return name; } - } - - internal Queue Queue { - get { return queue; } - } - - internal bool TryRecycle (TimeSpan maxIdleTime, ref DateTime idleSince) - { - var now = DateTime.UtcNow; - - again: - bool recycled; - List connectionsToClose = null; - - lock (sPoint) { - if (closing) { - idleSince = DateTime.MinValue; - return true; - } - - int count = 0; - var iter = connections.First; - while (iter != null) { - var cnc = iter.Value; - var node = iter; - iter = iter.Next; - - ++count; - if (cnc.Busy) - continue; - - if (count <= sPoint.ConnectionLimit && now - cnc.IdleSince < maxIdleTime) { - if (cnc.IdleSince > idleSince) - idleSince = cnc.IdleSince; - continue; - } - - /* - * Do not call WebConnection.Close() while holding the ServicePoint lock - * as this could deadlock when attempting to take the WebConnection lock. - * - */ - - if (connectionsToClose == null) - connectionsToClose = new List (); - connectionsToClose.Add (cnc.Connection); - connections.Remove (node); - } - - recycled = connections.Count == 0; - } - - // Did we find anything that can be closed? - if (connectionsToClose == null) - return recycled; - - // Ok, let's get rid of these! - foreach (var cnc in connectionsToClose) - cnc.Close (false); - - // Re-take the lock, then remove them from the connection list. - goto again; - } - - class ConnectionState : IWebConnectionState { - public WebConnection Connection { - get; - private set; - } - - public WebConnectionGroup Group { - get; - private set; - } - - public ServicePoint ServicePoint { - get { return Group.sPoint; } - } - - bool busy; - DateTime idleSince; - - public bool Busy { - get { return busy; } - } - - public DateTime IdleSince { - get { return idleSince; } - } - - public bool TrySetBusy () - { - lock (ServicePoint) { - if (busy) - return false; - busy = true; - idleSince = DateTime.UtcNow + TimeSpan.FromDays (3650); - return true; - } - } - - public void SetIdle () - { - lock (ServicePoint) { - busy = false; - idleSince = DateTime.UtcNow; - } - } - - public ConnectionState (WebConnectionGroup group) - { - Group = group; - idleSince = DateTime.UtcNow; - Connection = new WebConnection (this, group.sPoint); - } - } - - } -} - diff --git a/mcs/class/System/System.Net/WebConnectionStream.cs b/mcs/class/System/System.Net/WebConnectionStream.cs index 96a2b3bf42fc..f382ac90fccd 100644 --- a/mcs/class/System/System.Net/WebConnectionStream.cs +++ b/mcs/class/System/System.Net/WebConnectionStream.cs @@ -3,9 +3,11 @@ // // Authors: // Gonzalo Paniagua Javier (gonzalo@ximian.com) +// Martin Baulig // // (C) 2003 Ximian, Inc (http://www.ximian.com) // (C) 2004 Novell, Inc (http://www.novell.com) +// Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com) // // @@ -28,127 +30,53 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // - using System.IO; using System.Text; using System.Threading; +using System.Threading.Tasks; +using System.Runtime.ExceptionServices; +using System.Net.Sockets; namespace System.Net { - class WebConnectionStream : Stream + abstract class WebConnectionStream : Stream { - static byte [] crlf = new byte [] { 13, 10 }; - bool isRead; - WebConnection cnc; - HttpWebRequest request; - byte [] readBuffer; - int readBufferOffset; - int readBufferSize; - int stream_length; // -1 when CL not present - long contentLength; - long totalRead; - internal long totalWritten; - bool nextReadCalled; - int pendingReads; - int pendingWrites; - ManualResetEvent pending; - bool allowBuffering; - bool sendChunked; - MemoryStream writeBuffer; - bool requestWritten; - byte [] headers; + protected bool closed; bool disposed; - bool headersSent; object locker = new object (); - bool initRead; - bool read_eof; - bool complete_request_written; int read_timeout; int write_timeout; - AsyncCallback cb_wrapper; // Calls to ReadCallbackWrapper or WriteCallbacWrapper internal bool IgnoreIOErrors; - public WebConnectionStream (WebConnection cnc, WebConnectionData data) - { - if (data == null) - throw new InvalidOperationException ("data was not initialized"); - if (data.Headers == null) - throw new InvalidOperationException ("data.Headers was not initialized"); - if (data.request == null) - throw new InvalidOperationException ("data.request was not initialized"); - isRead = true; - cb_wrapper = new AsyncCallback (ReadCallbackWrapper); - pending = new ManualResetEvent (true); - this.request = data.request; - read_timeout = request.ReadWriteTimeout; - write_timeout = read_timeout; - this.cnc = cnc; - string contentType = data.Headers ["Transfer-Encoding"]; - bool chunkedRead = (contentType != null && contentType.IndexOf ("chunked", StringComparison.OrdinalIgnoreCase) != -1); - string clength = data.Headers ["Content-Length"]; - if (!chunkedRead && clength != null && clength != "") { - try { - contentLength = Int32.Parse (clength); - if (contentLength == 0 && !IsNtlmAuth ()) { - ReadAll (); - } - } catch { - contentLength = Int64.MaxValue; - } - } else { - contentLength = Int64.MaxValue; - } - - // Negative numbers? - if (!Int32.TryParse (clength, out stream_length)) - stream_length = -1; - } - - public WebConnectionStream (WebConnection cnc, HttpWebRequest request) + protected WebConnectionStream (WebConnection cnc, WebOperation operation, Stream stream) { - read_timeout = request.ReadWriteTimeout; + Connection = cnc; + Operation = operation; + Request = operation.Request; + InnerStream = stream; + + read_timeout = Request.ReadWriteTimeout; write_timeout = read_timeout; - isRead = false; - cb_wrapper = new AsyncCallback (WriteCallbackWrapper); - this.cnc = cnc; - this.request = request; - allowBuffering = request.InternalAllowBuffering; - sendChunked = request.SendChunked; - if (sendChunked) - pending = new ManualResetEvent (true); - else if (allowBuffering) - writeBuffer = new MemoryStream (); } - bool CheckAuthHeader (string headerName) - { - var authHeader = cnc.Data.Headers [headerName]; - return (authHeader != null && authHeader.IndexOf ("NTLM", StringComparison.Ordinal) != -1); + internal HttpWebRequest Request { + get; } - bool IsNtlmAuth () - { - bool isProxy = (request.Proxy != null && !request.Proxy.IsBypassed (request.Address)); - if (isProxy && CheckAuthHeader ("Proxy-Authenticate")) - return true; - return CheckAuthHeader ("WWW-Authenticate"); + internal WebConnection Connection { + get; } - internal void CheckResponseInBuffer () - { - if (contentLength > 0 && (readBufferSize - readBufferOffset) >= contentLength) { - if (!IsNtlmAuth ()) - ReadAll (); - } + internal WebOperation Operation { + get; } - internal HttpWebRequest Request { - get { return request; } - } + internal ServicePoint ServicePoint => Connection.ServicePoint; - internal WebConnection Connection { - get { return cnc; } + internal Stream InnerStream { + get; } + public override bool CanTimeout { get { return true; } } @@ -177,579 +105,126 @@ public override int WriteTimeout { } } - internal bool CompleteRequestWritten { - get { return complete_request_written; } - } - - internal bool SendChunked { - set { sendChunked = value; } - } - - internal byte [] ReadBuffer { - set { readBuffer = value; } - } - - internal int ReadBufferOffset { - set { readBufferOffset = value; } - } - - internal int ReadBufferSize { - set { readBufferSize = value; } - } - - internal byte[] WriteBuffer { - get { return writeBuffer.GetBuffer (); } - } - - internal int WriteBufferLength { - get { return writeBuffer != null ? (int) writeBuffer.Length : (-1); } - } - - internal void ForceCompletion () - { - if (!nextReadCalled) { - if (contentLength == Int64.MaxValue) - contentLength = 0; - nextReadCalled = true; - cnc.NextRead (); - } - } - - internal void CheckComplete () + protected Exception GetException (Exception e) { - bool nrc = nextReadCalled; - if (!nrc && readBufferSize - readBufferOffset == contentLength) { - nextReadCalled = true; - cnc.NextRead (); - } + e = HttpWebRequest.FlattenException (e); + if (e is WebException) + return e; + if (Operation.Aborted || e is OperationCanceledException || e is ObjectDisposedException) + return HttpWebRequest.CreateRequestAbortedException (); + return e; } - internal void ReadAll () + public override int Read (byte[] buffer, int offset, int size) { - if (!isRead || read_eof || totalRead >= contentLength || nextReadCalled) { - if (isRead && !nextReadCalled) { - nextReadCalled = true; - cnc.NextRead (); - } - return; - } - - if (!pending.WaitOne (ReadTimeout)) - throw new WebException ("The operation has timed out.", WebExceptionStatus.Timeout); - lock (locker) { - if (totalRead >= contentLength) - return; - - byte [] b = null; - int diff = readBufferSize - readBufferOffset; - int new_size; - - if (contentLength == Int64.MaxValue) { - MemoryStream ms = new MemoryStream (); - byte [] buffer = null; - if (readBuffer != null && diff > 0) { - ms.Write (readBuffer, readBufferOffset, diff); - if (readBufferSize >= 8192) - buffer = readBuffer; - } - - if (buffer == null) - buffer = new byte [8192]; - - int read; - while ((read = cnc.Read (request, buffer, 0, buffer.Length)) != 0) - ms.Write (buffer, 0, read); - - b = ms.GetBuffer (); - new_size = (int) ms.Length; - contentLength = new_size; - } else { - new_size = (int) (contentLength - totalRead); - b = new byte [new_size]; - if (readBuffer != null && diff > 0) { - if (diff > new_size) - diff = new_size; - - Buffer.BlockCopy (readBuffer, readBufferOffset, b, 0, diff); - } - - int remaining = new_size - diff; - int r = -1; - while (remaining > 0 && r != 0) { - r = cnc.Read (request, b, diff, remaining); - remaining -= r; - diff += r; - } - } - - readBuffer = b; - readBufferOffset = 0; - readBufferSize = new_size; - totalRead = 0; - nextReadCalled = true; - } + if (!CanRead) + throw new NotSupportedException (SR.net_writeonlystream); + Operation.ThrowIfClosedOrDisposed (); - cnc.NextRead (); - } - - void WriteCallbackWrapper (IAsyncResult r) - { - WebAsyncResult result = r as WebAsyncResult; - if (result != null && result.AsyncWriteAll) - return; - - if (r.AsyncState != null) { - result = (WebAsyncResult) r.AsyncState; - result.InnerAsyncResult = r; - result.DoCallback (); - } else { - try { - EndWrite (r); - } catch { - } - } - } + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); - void ReadCallbackWrapper (IAsyncResult r) - { - WebAsyncResult result; - if (r.AsyncState != null) { - result = (WebAsyncResult) r.AsyncState; - result.InnerAsyncResult = r; - result.DoCallback (); - } else { - try { - EndRead (r); - } catch { - } - } - } + int length = buffer.Length; + if (offset < 0 || length < offset) + throw new ArgumentOutOfRangeException (nameof (offset)); + if (size < 0 || (length - offset) < size) + throw new ArgumentOutOfRangeException (nameof (size)); - public override int Read (byte [] buffer, int offset, int size) - { - AsyncCallback cb = cb_wrapper; - WebAsyncResult res = (WebAsyncResult) BeginRead (buffer, offset, size, cb, null); - if (!res.IsCompleted && !res.WaitUntilComplete (ReadTimeout, false)) { - nextReadCalled = true; - cnc.Close (true); - throw new WebException ("The operation has timed out.", WebExceptionStatus.Timeout); + try { + return ReadAsync (buffer, offset, size, CancellationToken.None).Result; + } catch (Exception e) { + throw GetException (e); } - - return EndRead (res); } - public override IAsyncResult BeginRead (byte [] buffer, int offset, int size, + public override IAsyncResult BeginRead (byte[] buffer, int offset, int size, AsyncCallback cb, object state) { - if (!isRead) - throw new NotSupportedException ("this stream does not allow reading"); + if (!CanRead) + throw new NotSupportedException (SR.net_writeonlystream); + Operation.ThrowIfClosedOrDisposed (); if (buffer == null) - throw new ArgumentNullException ("buffer"); + throw new ArgumentNullException (nameof (buffer)); int length = buffer.Length; if (offset < 0 || length < offset) - throw new ArgumentOutOfRangeException ("offset"); + throw new ArgumentOutOfRangeException (nameof (offset)); if (size < 0 || (length - offset) < size) - throw new ArgumentOutOfRangeException ("size"); + throw new ArgumentOutOfRangeException (nameof (size)); - lock (locker) { - pendingReads++; - pending.Reset (); - } - - WebAsyncResult result = new WebAsyncResult (cb, state, buffer, offset, size); - if (totalRead >= contentLength) { - result.SetCompleted (true, -1); - result.DoCallback (); - return result; - } - - int remaining = readBufferSize - readBufferOffset; - if (remaining > 0) { - int copy = (remaining > size) ? size : remaining; - Buffer.BlockCopy (readBuffer, readBufferOffset, buffer, offset, copy); - readBufferOffset += copy; - offset += copy; - size -= copy; - totalRead += copy; - if (size == 0 || totalRead >= contentLength) { - result.SetCompleted (true, copy); - result.DoCallback (); - return result; - } - result.NBytes = copy; - } - - if (cb != null) - cb = cb_wrapper; - - if (contentLength != Int64.MaxValue && contentLength - totalRead < size) - size = (int)(contentLength - totalRead); - - if (!read_eof) { - result.InnerAsyncResult = cnc.BeginRead (request, buffer, offset, size, cb, result); - } else { - result.SetCompleted (true, result.NBytes); - result.DoCallback (); - } - return result; + var task = ReadAsync (buffer, offset, size, CancellationToken.None); + return TaskToApm.Begin (task, cb, state); } public override int EndRead (IAsyncResult r) { - WebAsyncResult result = (WebAsyncResult) r; - if (result.EndCalled) { - int xx = result.NBytes; - return (xx >= 0) ? xx : 0; - } - - result.EndCalled = true; - - if (!result.IsCompleted) { - int nbytes = -1; - try { - nbytes = cnc.EndRead (request, result); - } catch (Exception exc) { - lock (locker) { - pendingReads--; - if (pendingReads == 0) - pending.Set (); - } - - nextReadCalled = true; - cnc.Close (true); - result.SetCompleted (false, exc); - result.DoCallback (); - throw; - } - - if (nbytes < 0) { - nbytes = 0; - read_eof = true; - } - - totalRead += nbytes; - result.SetCompleted (false, nbytes + result.NBytes); - result.DoCallback (); - if (nbytes == 0) - contentLength = totalRead; - } - - lock (locker) { - pendingReads--; - if (pendingReads == 0) - pending.Set (); - } - - if (totalRead >= contentLength && !nextReadCalled) - ReadAll (); - - int nb = result.NBytes; - return (nb >= 0) ? nb : 0; - } - - void WriteAsyncCB (IAsyncResult r) - { - WebAsyncResult result = (WebAsyncResult) r.AsyncState; - result.InnerAsyncResult = null; + if (r == null) + throw new ArgumentNullException (nameof (r)); try { - cnc.EndWrite (request, true, r); - result.SetCompleted (false, 0); - if (!initRead) { - initRead = true; - cnc.InitRead (); - } + return TaskToApm.End (r); } catch (Exception e) { - KillBuffer (); - nextReadCalled = true; - cnc.Close (true); - if (e is System.Net.Sockets.SocketException) - e = new IOException ("Error writing request", e); - result.SetCompleted (false, e); + throw GetException (e); } - - if (allowBuffering && !sendChunked && request.ContentLength > 0 && totalWritten == request.ContentLength) - complete_request_written = true; - - result.DoCallback (); } - public override IAsyncResult BeginWrite (byte [] buffer, int offset, int size, - AsyncCallback cb, object state) + public override IAsyncResult BeginWrite (byte[] buffer, int offset, int size, + AsyncCallback cb, object state) { - if (request.Aborted) - throw new WebException ("The request was canceled.", WebExceptionStatus.RequestCanceled); - - if (isRead) - throw new NotSupportedException ("this stream does not allow writing"); + if (!CanWrite) + throw new NotSupportedException (SR.net_readonlystream); + Operation.ThrowIfClosedOrDisposed (); if (buffer == null) - throw new ArgumentNullException ("buffer"); + throw new ArgumentNullException (nameof (buffer)); int length = buffer.Length; if (offset < 0 || length < offset) - throw new ArgumentOutOfRangeException ("offset"); + throw new ArgumentOutOfRangeException (nameof (offset)); if (size < 0 || (length - offset) < size) - throw new ArgumentOutOfRangeException ("size"); - - if (sendChunked) { - lock (locker) { - pendingWrites++; - pending.Reset (); - } - } - - WebAsyncResult result = new WebAsyncResult (cb, state); - AsyncCallback callback = new AsyncCallback (WriteAsyncCB); - - if (sendChunked) { - requestWritten = true; - - string cSize = String.Format ("{0:X}\r\n", size); - byte[] head = Encoding.ASCII.GetBytes (cSize); - int chunkSize = 2 + size + head.Length; - byte[] newBuffer = new byte [chunkSize]; - Buffer.BlockCopy (head, 0, newBuffer, 0, head.Length); - Buffer.BlockCopy (buffer, offset, newBuffer, head.Length, size); - Buffer.BlockCopy (crlf, 0, newBuffer, head.Length + size, crlf.Length); - - if (allowBuffering) { - if (writeBuffer == null) - writeBuffer = new MemoryStream (); - writeBuffer.Write (buffer, offset, size); - totalWritten += size; - } - - buffer = newBuffer; - offset = 0; - size = chunkSize; - } else { - CheckWriteOverflow (request.ContentLength, totalWritten, size); - - if (allowBuffering) { - if (writeBuffer == null) - writeBuffer = new MemoryStream (); - writeBuffer.Write (buffer, offset, size); - totalWritten += size; - - if (request.ContentLength <= 0 || totalWritten < request.ContentLength) { - result.SetCompleted (true, 0); - result.DoCallback (); - return result; - } - - result.AsyncWriteAll = true; - requestWritten = true; - buffer = writeBuffer.GetBuffer (); - offset = 0; - size = (int)totalWritten; - } - } + throw new ArgumentOutOfRangeException (nameof (size)); - try { - result.InnerAsyncResult = cnc.BeginWrite (request, buffer, offset, size, callback, result); - if (result.InnerAsyncResult == null) { - if (!result.IsCompleted) - result.SetCompleted (true, 0); - result.DoCallback (); - } - } catch (Exception) { - if (!IgnoreIOErrors) - throw; - result.SetCompleted (true, 0); - result.DoCallback (); - } - totalWritten += size; - return result; - } - - void CheckWriteOverflow (long contentLength, long totalWritten, long size) - { - if (contentLength == -1) - return; - - long avail = contentLength - totalWritten; - if (size > avail) { - KillBuffer (); - nextReadCalled = true; - cnc.Close (true); - throw new ProtocolViolationException ( - "The number of bytes to be written is greater than " + - "the specified ContentLength."); - } + var task = WriteAsync (buffer, offset, size, CancellationToken.None); + return TaskToApm.Begin (task, cb, state); } public override void EndWrite (IAsyncResult r) { if (r == null) - throw new ArgumentNullException ("r"); - - WebAsyncResult result = r as WebAsyncResult; - if (result == null) - throw new ArgumentException ("Invalid IAsyncResult"); - - if (result.EndCalled) - return; - - if (sendChunked) { - lock (locker) { - pendingWrites--; - if (pendingWrites <= 0) - pending.Set (); - } - } + throw new ArgumentNullException (nameof (r)); - result.EndCalled = true; - if (result.AsyncWriteAll) { - result.WaitUntilComplete (); - if (result.GotException) - throw result.Exception; - return; - } - - if (allowBuffering && !sendChunked) - return; - - if (result.GotException) - throw result.Exception; - } - - public override void Write (byte [] buffer, int offset, int size) - { - AsyncCallback cb = cb_wrapper; - WebAsyncResult res = (WebAsyncResult) BeginWrite (buffer, offset, size, cb, null); - if (!res.IsCompleted && !res.WaitUntilComplete (WriteTimeout, false)) { - KillBuffer (); - nextReadCalled = true; - cnc.Close (true); - throw new IOException ("Write timed out."); + try { + TaskToApm.End (r); + } catch (Exception e) { + throw GetException (e); } - - EndWrite (res); - } - - public override void Flush () - { - } - - internal void SetHeadersAsync (bool setInternalLength, SimpleAsyncCallback callback) - { - SimpleAsyncResult.Run (r => SetHeadersAsync (r, setInternalLength), callback); } - bool SetHeadersAsync (SimpleAsyncResult result, bool setInternalLength) + public override void Write (byte[] buffer, int offset, int size) { - if (headersSent) - return false; - - string method = request.Method; - bool no_writestream = (method == "GET" || method == "CONNECT" || method == "HEAD" || - method == "TRACE"); - bool webdav = (method == "PROPFIND" || method == "PROPPATCH" || method == "MKCOL" || - method == "COPY" || method == "MOVE" || method == "LOCK" || - method == "UNLOCK"); - - if (setInternalLength && !no_writestream && writeBuffer != null) - request.InternalContentLength = writeBuffer.Length; + if (!CanWrite) + throw new NotSupportedException (SR.net_readonlystream); + Operation.ThrowIfClosedOrDisposed (); - bool has_content = !no_writestream && (writeBuffer == null || request.ContentLength > -1); - if (!(sendChunked || has_content || no_writestream || webdav)) - return false; - - headersSent = true; - headers = request.GetRequestHeaders (); - - var innerResult = cnc.BeginWrite (request, headers, 0, headers.Length, r => { - try { - cnc.EndWrite (request, true, r); - if (!initRead) { - initRead = true; - cnc.InitRead (); - } - var cl = request.ContentLength; - if (!sendChunked && cl == 0) - requestWritten = true; - result.SetCompleted (false); - } catch (WebException e) { - result.SetCompleted (false, e); - } catch (Exception e) { - result.SetCompleted (false, new WebException ("Error writing headers", WebExceptionStatus.SendFailure, WebExceptionInternalStatus.RequestFatal, e)); - } - }, null); - - return innerResult != null; - } + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); - internal bool RequestWritten { - get { return requestWritten; } - } + int length = buffer.Length; + if (offset < 0 || length < offset) + throw new ArgumentOutOfRangeException (nameof (offset)); + if (size < 0 || (length - offset) < size) + throw new ArgumentOutOfRangeException (nameof (size)); - internal SimpleAsyncResult WriteRequestAsync (SimpleAsyncCallback callback) - { - var result = WriteRequestAsync (callback); try { - if (!WriteRequestAsync (result)) - result.SetCompleted (true); - } catch (Exception ex) { - result.SetCompleted (true, ex); + WriteAsync (buffer, offset, size).Wait (); + } catch (Exception e) { + throw GetException (e); } - return result; } - internal bool WriteRequestAsync (SimpleAsyncResult result) + public override void Flush () { - if (requestWritten) - return false; - - requestWritten = true; - if (sendChunked || !allowBuffering || writeBuffer == null) - return false; - - // Keep the call for a potential side-effect of GetBuffer - var bytes = writeBuffer.GetBuffer (); - var length = (int)writeBuffer.Length; - if (request.ContentLength != -1 && request.ContentLength < length) { - nextReadCalled = true; - cnc.Close (true); - throw new WebException ("Specified Content-Length is less than the number of bytes to write", null, - WebExceptionStatus.ServerProtocolViolation, null); - } - - SetHeadersAsync (true, inner => { - if (inner.GotException) { - result.SetCompleted (inner.CompletedSynchronouslyPeek, inner.Exception); - return; - } - - if (cnc.Data.StatusCode != 0 && cnc.Data.StatusCode != 100) { - result.SetCompleted (inner.CompletedSynchronouslyPeek); - return; - } - - if (!initRead) { - initRead = true; - cnc.InitRead (); - } - - if (length == 0) { - complete_request_written = true; - result.SetCompleted (inner.CompletedSynchronouslyPeek); - return; - } - - cnc.BeginWrite (request, bytes, 0, length, r => { - try { - complete_request_written = cnc.EndWrite (request, false, r); - result.SetCompleted (false); - } catch (Exception exc) { - result.SetCompleted (false, exc); - } - }, null); - }); - - return true; } internal void InternalClose () @@ -757,103 +232,26 @@ internal void InternalClose () disposed = true; } - internal bool GetResponseOnClose { - get; set; - } + protected abstract void Close_internal (ref bool disposed); public override void Close () { - if (GetResponseOnClose) { - if (disposed) - return; - disposed = true; - var response = (HttpWebResponse)request.GetResponse (); - response.ReadAll (); - response.Close (); - return; - } - - if (sendChunked) { - if (disposed) - return; - disposed = true; - if (!pending.WaitOne (WriteTimeout)) { - throw new WebException ("The operation has timed out.", WebExceptionStatus.Timeout); - } - byte [] chunk = Encoding.ASCII.GetBytes ("0\r\n\r\n"); - string err_msg = null; - cnc.Write (request, chunk, 0, chunk.Length, ref err_msg); - return; - } - - if (isRead) { - if (!nextReadCalled) { - CheckComplete (); - // If we have not read all the contents - if (!nextReadCalled) { - nextReadCalled = true; - cnc.Close (true); - } - } - return; - } else if (!allowBuffering) { - complete_request_written = true; - if (!initRead) { - initRead = true; - cnc.InitRead (); - } - return; - } - - if (disposed || requestWritten) - return; - - long length = request.ContentLength; - - if (!sendChunked && length != -1 && totalWritten != length) { - IOException io = new IOException ("Cannot close the stream until all bytes are written"); - nextReadCalled = true; - cnc.Close (true); - throw new WebException ("Request was cancelled.", WebExceptionStatus.RequestCanceled, WebExceptionInternalStatus.RequestFatal, io); - } - - // Commented out the next line to fix xamarin bug #1512 - //WriteRequest (); - disposed = true; - } - - internal void KillBuffer () - { - writeBuffer = null; + Close_internal (ref disposed); } public override long Seek (long a, SeekOrigin b) { throw new NotSupportedException (); } - + public override void SetLength (long a) { throw new NotSupportedException (); } - - public override bool CanSeek { - get { return false; } - } - - public override bool CanRead { - get { return !disposed && isRead; } - } - public override bool CanWrite { - get { return !disposed && !isRead; } - } - - public override long Length { + public override bool CanSeek { get { - if (!isRead) - throw new NotSupportedException (); - return stream_length; + return false; } } diff --git a/mcs/class/System/System.Net/WebConnectionTunnel.cs b/mcs/class/System/System.Net/WebConnectionTunnel.cs new file mode 100644 index 000000000000..826d59df922f --- /dev/null +++ b/mcs/class/System/System.Net/WebConnectionTunnel.cs @@ -0,0 +1,271 @@ +// +// System.Net.WebConnectionTunnel +// +// Authors: +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// Martin Baulig +// +// (C) 2003 Ximian, Inc (http://www.ximian.com) +// Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com) +// +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +using System.IO; +using System.Collections; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.ExceptionServices; +using System.Diagnostics; +using Mono.Net.Security; + +namespace System.Net +{ + class WebConnectionTunnel + { + public HttpWebRequest Request { + get; + } + + public Uri ConnectUri { + get; + } + + public WebConnectionTunnel (HttpWebRequest request, Uri connectUri) + { + Request = request; + ConnectUri = connectUri; + } + + enum NtlmAuthState + { + None, + Challenge, + Response + } + + HttpWebRequest connectRequest; + NtlmAuthState ntlmAuthState; + + public bool Success { + get; + private set; + } + + public bool CloseConnection { + get; + private set; + } + + public int StatusCode { + get; + private set; + } + + public string StatusDescription { + get; + private set; + } + + public string[] Challenge { + get; + private set; + } + + public WebHeaderCollection Headers { + get; + private set; + } + + public Version ProxyVersion { + get; + private set; + } + + public byte[] Data { + get; + private set; + } + + internal async Task Initialize (Stream stream, CancellationToken cancellationToken) + { + StringBuilder sb = new StringBuilder (); + sb.Append ("CONNECT "); + sb.Append (Request.Address.Host); + sb.Append (':'); + sb.Append (Request.Address.Port); + sb.Append (" HTTP/"); + if (Request.ProtocolVersion == HttpVersion.Version11) + sb.Append ("1.1"); + else + sb.Append ("1.0"); + + sb.Append ("\r\nHost: "); + sb.Append (Request.Address.Authority); + + bool ntlm = false; + var challenge = Challenge; + Challenge = null; + var auth_header = Request.Headers["Proxy-Authorization"]; + bool have_auth = auth_header != null; + if (have_auth) { + sb.Append ("\r\nProxy-Authorization: "); + sb.Append (auth_header); + ntlm = auth_header.ToUpper ().Contains ("NTLM"); + } else if (challenge != null && StatusCode == 407) { + ICredentials creds = Request.Proxy.Credentials; + have_auth = true; + + if (connectRequest == null) { + // create a CONNECT request to use with Authenticate + connectRequest = (HttpWebRequest)WebRequest.Create ( + ConnectUri.Scheme + "://" + ConnectUri.Host + ":" + ConnectUri.Port + "/"); + connectRequest.Method = "CONNECT"; + connectRequest.Credentials = creds; + } + + if (creds != null) { + for (int i = 0; i < challenge.Length; i++) { + var auth = AuthenticationManager.Authenticate (challenge[i], connectRequest, creds); + if (auth == null) + continue; + ntlm = (auth.ModuleAuthenticationType == "NTLM"); + sb.Append ("\r\nProxy-Authorization: "); + sb.Append (auth.Message); + break; + } + } + } + + if (ntlm) { + sb.Append ("\r\nProxy-Connection: keep-alive"); + ntlmAuthState++; + } + + sb.Append ("\r\n\r\n"); + + StatusCode = 0; + byte[] connectBytes = Encoding.Default.GetBytes (sb.ToString ()); + await stream.WriteAsync (connectBytes, 0, connectBytes.Length, cancellationToken).ConfigureAwait (false); + + (Headers, Data, StatusCode) = await ReadHeaders (stream, cancellationToken).ConfigureAwait (false); + + if ((!have_auth || ntlmAuthState == NtlmAuthState.Challenge) && Headers != null && StatusCode == 407) { // Needs proxy auth + var connectionHeader = Headers["Connection"]; + if (!string.IsNullOrEmpty (connectionHeader) && connectionHeader.ToLower () == "close") { + // The server is requesting that this connection be closed + CloseConnection = true; + } + + Challenge = Headers.GetValues ("Proxy-Authenticate"); + Success = false; + } else { + Success = StatusCode == 200 && Headers != null; + } + + if (Challenge == null && (StatusCode == 401 || StatusCode == 407)) { + var response = new HttpWebResponse (ConnectUri, "CONNECT", (HttpStatusCode)StatusCode, Headers); + throw new WebException ( + StatusCode == 407 ? "(407) Proxy Authentication Required" : "(401) Unauthorized", + null, WebExceptionStatus.ProtocolError, response); + } + } + + async Task<(WebHeaderCollection, byte[], int)> ReadHeaders (Stream stream, CancellationToken cancellationToken) + { + byte[] retBuffer = null; + int status = 200; + + byte[] buffer = new byte[1024]; + MemoryStream ms = new MemoryStream (); + + while (true) { + cancellationToken.ThrowIfCancellationRequested (); + int n = await stream.ReadAsync (buffer, 0, 1024, cancellationToken).ConfigureAwait (false); + if (n == 0) + throw WebConnection.GetException (WebExceptionStatus.ServerProtocolViolation, null); + + ms.Write (buffer, 0, n); + int start = 0; + string str = null; + bool gotStatus = false; + WebHeaderCollection headers = new WebHeaderCollection (); + while (WebConnection.ReadLine (ms.GetBuffer (), ref start, (int)ms.Length, ref str)) { + if (str == null) { + int contentLen; + var clengthHeader = headers["Content-Length"]; + if (string.IsNullOrEmpty (clengthHeader) || !int.TryParse (clengthHeader, out contentLen)) + contentLen = 0; + + if (ms.Length - start - contentLen > 0) { + // we've read more data than the response header and conents, + // give back extra data to the caller + retBuffer = new byte[ms.Length - start - contentLen]; + Buffer.BlockCopy (ms.GetBuffer (), start + contentLen, retBuffer, 0, retBuffer.Length); + } else { + // haven't read in some or all of the contents for the response, do so now + FlushContents (stream, contentLen - (int)(ms.Length - start)); + } + + return (headers, retBuffer, status); + } + + if (gotStatus) { + headers.Add (str); + continue; + } + + string[] parts = str.Split (' '); + if (parts.Length < 2) + throw WebConnection.GetException (WebExceptionStatus.ServerProtocolViolation, null); + + if (String.Compare (parts[0], "HTTP/1.1", true) == 0) + ProxyVersion = HttpVersion.Version11; + else if (String.Compare (parts[0], "HTTP/1.0", true) == 0) + ProxyVersion = HttpVersion.Version10; + else + throw WebConnection.GetException (WebExceptionStatus.ServerProtocolViolation, null); + + status = (int)UInt32.Parse (parts[1]); + if (parts.Length >= 3) + StatusDescription = String.Join (" ", parts, 2, parts.Length - 2); + + gotStatus = true; + } + } + } + + void FlushContents (Stream stream, int contentLength) + { + while (contentLength > 0) { + byte[] contentBuffer = new byte[contentLength]; + int bytesRead = stream.Read (contentBuffer, 0, contentLength); + if (bytesRead > 0) { + contentLength -= bytesRead; + } else { + break; + } + } + } + } +} \ No newline at end of file diff --git a/mcs/class/System/System.Net/WebOperation.cs b/mcs/class/System/System.Net/WebOperation.cs new file mode 100644 index 000000000000..0405c00c1392 --- /dev/null +++ b/mcs/class/System/System.Net/WebOperation.cs @@ -0,0 +1,344 @@ +// +// WebOperation.cs +// +// Author: +// Martin Baulig +// +// Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System.IO; +using System.Collections; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.ExceptionServices; +using System.Diagnostics; + +namespace System.Net +{ + class WebOperation + { + public HttpWebRequest Request { + get; + } + + public WebConnection Connection { + get; + private set; + } + + public ServicePoint ServicePoint { + get; + private set; + } + + public BufferOffsetSize WriteBuffer { + get; + } + + public bool IsNtlmChallenge { + get; + } + +#if MONO_WEB_DEBUG + static int nextID; + internal readonly int ID = ++nextID; +#else + internal readonly int ID; +#endif + + public WebOperation (HttpWebRequest request, BufferOffsetSize writeBuffer, bool isNtlmChallenge, CancellationToken cancellationToken) + { + Request = request; + WriteBuffer = writeBuffer; + IsNtlmChallenge = isNtlmChallenge; + cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); + requestTask = new TaskCompletionSource (); + requestWrittenTask = new TaskCompletionSource (); + completeResponseReadTask = new TaskCompletionSource (); + responseTask = new TaskCompletionSource (); + finishedTask = new TaskCompletionSource<(bool, WebOperation)> (); + } + + CancellationTokenSource cts; + TaskCompletionSource requestTask; + TaskCompletionSource requestWrittenTask; + TaskCompletionSource responseTask; + TaskCompletionSource completeResponseReadTask; + TaskCompletionSource<(bool, WebOperation)> finishedTask; + WebRequestStream writeStream; + WebResponseStream responseStream; + ExceptionDispatchInfo disposedInfo; + ExceptionDispatchInfo closedInfo; + WebOperation priorityRequest; + volatile bool finishedReading; + int requestSent; + + public bool Aborted { + get { + if (disposedInfo != null || Request.Aborted) + return true; + if (cts != null && cts.IsCancellationRequested) + return true; + return false; + } + } + + public bool Closed { + get { + return Aborted || closedInfo != null; + } + } + + public void Abort () + { + var (exception, disposed) = SetDisposed (ref disposedInfo); + if (!disposed) + return; + cts?.Cancel (); + SetCanceled (); + Close (); + } + + public void Close () + { + var (exception, closed) = SetDisposed (ref closedInfo); + if (!closed) + return; + + var stream = Interlocked.Exchange (ref writeStream, null); + if (stream != null) { + try { + stream.Close (); + } catch { } + } + } + + void SetCanceled () + { + requestTask.TrySetCanceled (); + requestWrittenTask.TrySetCanceled (); + responseTask.TrySetCanceled (); + completeResponseReadTask.TrySetCanceled (); + } + + void SetError (Exception error) + { + requestTask.TrySetException (error); + requestWrittenTask.TrySetException (error); + responseTask.TrySetException (error); + completeResponseReadTask.TrySetException (error); + } + + (ExceptionDispatchInfo, bool) SetDisposed (ref ExceptionDispatchInfo field) + { + var wexc = new WebException (SR.GetString (SR.net_webstatus_RequestCanceled), WebExceptionStatus.RequestCanceled); + var exception = ExceptionDispatchInfo.Capture (wexc); + var old = Interlocked.CompareExchange (ref field, exception, null); + return (old ?? exception, old == null); + } + + internal void ThrowIfDisposed () + { + ThrowIfDisposed (CancellationToken.None); + } + + internal void ThrowIfDisposed (CancellationToken cancellationToken) + { + if (Aborted || cancellationToken.IsCancellationRequested) + ThrowDisposed (ref disposedInfo); + } + + internal void ThrowIfClosedOrDisposed () + { + ThrowIfClosedOrDisposed (CancellationToken.None); + } + + internal void ThrowIfClosedOrDisposed (CancellationToken cancellationToken) + { + if (Closed || cancellationToken.IsCancellationRequested) + ThrowDisposed (ref closedInfo); + } + + void ThrowDisposed (ref ExceptionDispatchInfo field) + { + var (exception, disposed) = SetDisposed (ref field); + if (disposed) + cts?.Cancel (); + exception.Throw (); + } + + internal void RegisterRequest (ServicePoint servicePoint, WebConnection connection) + { + if (servicePoint == null) + throw new ArgumentNullException (nameof (servicePoint)); + if (connection == null) + throw new ArgumentNullException (nameof (connection)); + + lock (this) { + if (Interlocked.CompareExchange (ref requestSent, 1, 0) != 0) + throw new InvalidOperationException ("Invalid nested call."); + ServicePoint = servicePoint; + Connection = connection; + } + + cts.Token.Register (() => { + Request.FinishedReading = true; + SetDisposed (ref disposedInfo); + }); + } + + public void SetPriorityRequest (WebOperation operation) + { + lock (this) { + if (requestSent != 1 || ServicePoint == null || finishedReading) + throw new InvalidOperationException ("Should never happen."); + if (Interlocked.CompareExchange (ref priorityRequest, operation, null) != null) + throw new InvalidOperationException ("Invalid nested request."); + } + } + + public Task GetRequestStream () + { + return requestTask.Task; + } + + public Task WaitUntilRequestWritten () + { + return requestWrittenTask.Task; + } + + public WebRequestStream WriteStream { + get { + ThrowIfDisposed (); + return writeStream; + } + } + + public Task GetResponseStream () + { + return responseTask.Task; + } + + internal async Task<(bool, WebOperation)> WaitForCompletion (bool ignoreErrors) + { + try { + return await finishedTask.Task.ConfigureAwait (false); + } catch { + if (ignoreErrors) + return (false, null); + throw; + } + } + + internal async void Run () + { + try { + FinishReading (); + + ThrowIfClosedOrDisposed (); + + var requestStream = await Connection.InitConnection (this, cts.Token).ConfigureAwait (false); + + ThrowIfClosedOrDisposed (); + + writeStream = requestStream; + + await requestStream.Initialize (cts.Token).ConfigureAwait (false); + + ThrowIfClosedOrDisposed (); + + requestTask.TrySetResult (requestStream); + + var stream = new WebResponseStream (requestStream); + responseStream = stream; + + await stream.InitReadAsync (cts.Token).ConfigureAwait (false); + + responseTask.TrySetResult (stream); + } catch (OperationCanceledException) { + SetCanceled (); + } catch (Exception e) { + SetError (e); + } + } + + async void FinishReading () + { + bool ok = false; + Exception error = null; + + try { + ok = await completeResponseReadTask.Task.ConfigureAwait (false); + } catch (Exception e) { + error = e; + } + + WebResponseStream stream; + WebOperation next; + + lock (this) { + finishedReading = true; + stream = Interlocked.Exchange (ref responseStream, null); + next = Interlocked.Exchange (ref priorityRequest, null); + Request.FinishedReading = true; + } + + if (error != null) { + if (next != null) + next.SetError (error); + finishedTask.TrySetException (error); + return; + } + + WebConnection.Debug ($"WO FINISH READING: Cnc={Connection?.ID} Op={ID} ok={ok} error={error != null} stream={stream != null} next={next != null}"); + + var keepAlive = !Aborted && ok && (stream?.KeepAlive ?? false); + if (next != null && next.Aborted) { + next = null; + keepAlive = false; + } + + finishedTask.TrySetResult ((keepAlive, next)); + + WebConnection.Debug ($"WO FINISH READING DONE: Cnc={Connection.ID} Op={ID} - {keepAlive} next={next?.ID}"); + } + + internal void CompleteRequestWritten (WebRequestStream stream, Exception error = null) + { + WebConnection.Debug ($"WO COMPLETE REQUEST WRITTEN: Op={ID} {error != null}"); + + if (error != null) + SetError (error); + else + requestWrittenTask.TrySetResult (stream); + } + + internal void CompleteResponseRead (bool ok, Exception error = null) + { + WebConnection.Debug ($"WO COMPLETE RESPONSE READ: Op={ID} {ok} {error?.GetType ()}"); + + if (error != null) + completeResponseReadTask.TrySetException (error); + else + completeResponseReadTask.TrySetResult (ok); + } + } +} diff --git a/mcs/class/System/System.Net/WebRequestStream.cs b/mcs/class/System/System.Net/WebRequestStream.cs new file mode 100644 index 000000000000..c0cf5829764f --- /dev/null +++ b/mcs/class/System/System.Net/WebRequestStream.cs @@ -0,0 +1,436 @@ +// +// WebRequestStream.cs +// +// Author: +// Martin Baulig +// +// Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.ExceptionServices; +using System.Net.Sockets; + +namespace System.Net +{ + class WebRequestStream : WebConnectionStream + { + static byte[] crlf = new byte[] { 13, 10 }; + MemoryStream writeBuffer; + bool requestWritten; + bool allowBuffering; + bool sendChunked; + TaskCompletionSource pendingWrite; + long totalWritten; + byte[] headers; + bool headersSent; + int completeRequestWritten; + int chunkTrailerWritten; + + internal readonly string ME; + + public WebRequestStream (WebConnection connection, WebOperation operation, + Stream stream, WebConnectionTunnel tunnel) + : base (connection, operation, stream) + { + allowBuffering = operation.Request.InternalAllowBuffering; + sendChunked = operation.Request.SendChunked && operation.WriteBuffer == null; + if (!sendChunked && allowBuffering && operation.WriteBuffer == null) + writeBuffer = new MemoryStream (); + + KeepAlive = Request.KeepAlive; + if (tunnel?.ProxyVersion != null && tunnel?.ProxyVersion != HttpVersion.Version11) + KeepAlive = false; + +#if MONO_WEB_DEBUG + ME = $"WRQ(Cnc={Connection.ID}, Op={Operation.ID})"; +#endif + } + + public bool KeepAlive { + get; + } + + public override long Length { + get { + throw new NotSupportedException (); + } + } + + public override bool CanRead => false; + + public override bool CanWrite => true; + + internal bool SendChunked { + get { return sendChunked; } + set { sendChunked = value; } + } + + internal bool HasWriteBuffer { + get { + return Operation.WriteBuffer != null || writeBuffer != null; + } + } + + internal int WriteBufferLength { + get { + if (Operation.WriteBuffer != null) + return Operation.WriteBuffer.Size; + if (writeBuffer != null) + return (int)writeBuffer.Length; + return -1; + } + } + + internal BufferOffsetSize GetWriteBuffer () + { + if (Operation.WriteBuffer != null) + return Operation.WriteBuffer; + if (writeBuffer == null || writeBuffer.Length == 0) + return null; + var buffer = writeBuffer.GetBuffer (); + return new BufferOffsetSize (buffer, 0, (int)writeBuffer.Length, false); + } + + async Task FinishWriting (CancellationToken cancellationToken) + { + if (Interlocked.CompareExchange (ref completeRequestWritten, 1, 0) != 0) + return; + + WebConnection.Debug ($"{ME} FINISH WRITING: {sendChunked}"); + try { + Operation.ThrowIfClosedOrDisposed (cancellationToken); + if (sendChunked) + await WriteChunkTrailer_inner (cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + Operation.CompleteRequestWritten (this, ex); + throw; + } finally { + WebConnection.Debug ($"{ME} FINISH WRITING DONE"); + } + + Operation.CompleteRequestWritten (this); + } + + public override async Task WriteAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + WebConnection.Debug ($"{ME} WRITE ASYNC: {buffer.Length}/{offset}/{size}"); + + Operation.ThrowIfClosedOrDisposed (cancellationToken); + + if (Operation.WriteBuffer != null) + throw new InvalidOperationException (); + + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + int length = buffer.Length; + if (offset < 0 || length < offset) + throw new ArgumentOutOfRangeException (nameof (offset)); + if (size < 0 || (length - offset) < size) + throw new ArgumentOutOfRangeException (nameof (size)); + + var myWriteTcs = new TaskCompletionSource (); + if (Interlocked.CompareExchange (ref pendingWrite, myWriteTcs, null) != null) + throw new InvalidOperationException (SR.GetString (SR.net_repcall)); + + try { + await ProcessWrite (buffer, offset, size, cancellationToken).ConfigureAwait (false); + + WebConnection.Debug ($"{ME} WRITE ASYNC #1: {allowBuffering} {sendChunked} {Request.ContentLength} {totalWritten}"); + + if (Request.ContentLength > 0 && totalWritten == Request.ContentLength) + await FinishWriting (cancellationToken); + + pendingWrite = null; + myWriteTcs.TrySetResult (0); + } catch (Exception ex) { + KillBuffer (); + closed = true; + + WebConnection.Debug ($"{ME} WRITE ASYNC EX: {ex.Message}"); + + if (ex is SocketException) + ex = new IOException ("Error writing request", ex); + + Operation.CompleteRequestWritten (this, ex); + + pendingWrite = null; + myWriteTcs.TrySetException (ex); + throw; + } + } + + async Task ProcessWrite (byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + Operation.ThrowIfClosedOrDisposed (cancellationToken); + + if (sendChunked) { + requestWritten = true; + + string cSize = String.Format ("{0:X}\r\n", size); + byte[] head = Encoding.ASCII.GetBytes (cSize); + int chunkSize = 2 + size + head.Length; + byte[] newBuffer = new byte[chunkSize]; + Buffer.BlockCopy (head, 0, newBuffer, 0, head.Length); + Buffer.BlockCopy (buffer, offset, newBuffer, head.Length, size); + Buffer.BlockCopy (crlf, 0, newBuffer, head.Length + size, crlf.Length); + + if (allowBuffering) { + if (writeBuffer == null) + writeBuffer = new MemoryStream (); + writeBuffer.Write (buffer, offset, size); + } + + totalWritten += size; + + buffer = newBuffer; + offset = 0; + size = chunkSize; + } else { + CheckWriteOverflow (Request.ContentLength, totalWritten, size); + + if (allowBuffering) { + if (writeBuffer == null) + writeBuffer = new MemoryStream (); + writeBuffer.Write (buffer, offset, size); + totalWritten += size; + + if (Request.ContentLength <= 0 || totalWritten < Request.ContentLength) + return; + + requestWritten = true; + buffer = writeBuffer.GetBuffer (); + offset = 0; + size = (int)totalWritten; + } else { + totalWritten += size; + } + } + + try { + await InnerStream.WriteAsync (buffer, offset, size, cancellationToken).ConfigureAwait (false); + } catch { + if (!IgnoreIOErrors) + throw; + } + } + + void CheckWriteOverflow (long contentLength, long totalWritten, long size) + { + if (contentLength == -1) + return; + + long avail = contentLength - totalWritten; + if (size > avail) { + KillBuffer (); + closed = true; + var throwMe = new ProtocolViolationException ( + "The number of bytes to be written is greater than " + + "the specified ContentLength."); + Operation.CompleteRequestWritten (this, throwMe); + throw throwMe; + } + } + + internal async Task Initialize (CancellationToken cancellationToken) + { + Operation.ThrowIfClosedOrDisposed (cancellationToken); + + WebConnection.Debug ($"{ME} INIT: {Operation.WriteBuffer != null}"); + + if (Operation.WriteBuffer != null) { + if (Operation.IsNtlmChallenge) + Request.InternalContentLength = 0; + else + Request.InternalContentLength = Operation.WriteBuffer.Size; + } + + await SetHeadersAsync (false, cancellationToken).ConfigureAwait (false); + + Operation.ThrowIfClosedOrDisposed (cancellationToken); + + if (Operation.WriteBuffer != null && !Operation.IsNtlmChallenge) { + await WriteRequestAsync (cancellationToken); + Close (); + } + } + + async Task SetHeadersAsync (bool setInternalLength, CancellationToken cancellationToken) + { + Operation.ThrowIfClosedOrDisposed (cancellationToken); + + if (headersSent) + return; + + string method = Request.Method; + bool no_writestream = (method == "GET" || method == "CONNECT" || method == "HEAD" || + method == "TRACE"); + bool webdav = (method == "PROPFIND" || method == "PROPPATCH" || method == "MKCOL" || + method == "COPY" || method == "MOVE" || method == "LOCK" || + method == "UNLOCK"); + + if (Operation.IsNtlmChallenge) + no_writestream = true; + + if (setInternalLength && !no_writestream && HasWriteBuffer) + Request.InternalContentLength = WriteBufferLength; + + bool has_content = !no_writestream && (!HasWriteBuffer || Request.ContentLength > -1); + if (!(sendChunked || has_content || no_writestream || webdav)) + return; + + headersSent = true; + headers = Request.GetRequestHeaders (); + + WebConnection.Debug ($"{ME} SET HEADERS: {Request.ContentLength}"); + + try { + await InnerStream.WriteAsync (headers, 0, headers.Length, cancellationToken).ConfigureAwait (false); + var cl = Request.ContentLength; + if (!sendChunked && cl == 0) + requestWritten = true; + } catch (Exception e) { + if (e is WebException || e is OperationCanceledException) + throw; + throw new WebException ("Error writing headers", WebExceptionStatus.SendFailure, WebExceptionInternalStatus.RequestFatal, e); + } + } + + internal async Task WriteRequestAsync (CancellationToken cancellationToken) + { + Operation.ThrowIfClosedOrDisposed (cancellationToken); + + WebConnection.Debug ($"{ME} WRITE REQUEST: {requestWritten} {sendChunked} {allowBuffering} {HasWriteBuffer}"); + + if (requestWritten) + return; + + requestWritten = true; + if (sendChunked || !HasWriteBuffer) + return; + + BufferOffsetSize buffer = GetWriteBuffer (); + if (buffer != null && !Operation.IsNtlmChallenge && Request.ContentLength != -1 && Request.ContentLength < buffer.Size) { + closed = true; + var throwMe = new WebException ("Specified Content-Length is less than the number of bytes to write", null, + WebExceptionStatus.ServerProtocolViolation, null); + Operation.CompleteRequestWritten (this, throwMe); + throw throwMe; + } + + await SetHeadersAsync (true, cancellationToken).ConfigureAwait (false); + + WebConnection.Debug ($"{ME} WRITE REQUEST #1: {buffer != null}"); + + Operation.ThrowIfClosedOrDisposed (cancellationToken); + if (buffer != null && buffer.Size > 0) + await InnerStream.WriteAsync (buffer.Buffer, 0, buffer.Size, cancellationToken); + + await FinishWriting (cancellationToken); + } + + async Task WriteChunkTrailer_inner (CancellationToken cancellationToken) + { + if (Interlocked.CompareExchange (ref chunkTrailerWritten, 1, 0) != 0) + return; + Operation.ThrowIfClosedOrDisposed (cancellationToken); + byte[] chunk = Encoding.ASCII.GetBytes ("0\r\n\r\n"); + await InnerStream.WriteAsync (chunk, 0, chunk.Length, cancellationToken).ConfigureAwait (false); + } + + async Task WriteChunkTrailer () + { + using (var cts = new CancellationTokenSource ()) { + cts.CancelAfter (WriteTimeout); + var timeoutTask = Task.Delay (WriteTimeout); + while (true) { + var myWriteTcs = new TaskCompletionSource (); + var oldTcs = Interlocked.CompareExchange (ref pendingWrite, myWriteTcs, null); + if (oldTcs == null) + break; + var ret = await Task.WhenAny (timeoutTask, oldTcs.Task).ConfigureAwait (false); + if (ret == timeoutTask) + throw new WebException ("The operation has timed out.", WebExceptionStatus.Timeout); + } + + try { + await WriteChunkTrailer_inner (cts.Token).ConfigureAwait (false); + } catch { + // Intentionally eating exceptions. + } finally { + pendingWrite = null; + } + } + } + + internal void KillBuffer () + { + writeBuffer = null; + } + + public override Task ReadAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + return Task.FromException (new NotSupportedException (SR.net_writeonlystream)); + } + + protected override void Close_internal (ref bool disposed) + { + WebConnection.Debug ($"{ME} CLOSE: {disposed} {requestWritten} {allowBuffering}"); + + if (disposed) + return; + disposed = true; + + if (sendChunked) { + // Don't use FinishWriting() here, we need to block on 'pendingWrite' to ensure that + // any pending WriteAsync() has been completed. + // + // FIXME: I belive .NET simply aborts if you call Close() or Dispose() while writing, + // need to check this. 2017/07/17 Martin. + WriteChunkTrailer ().Wait (); + return; + } + + if (!allowBuffering || requestWritten) { + Operation.CompleteRequestWritten (this); + return; + } + + long length = Request.ContentLength; + + if (!sendChunked && !Operation.IsNtlmChallenge && length != -1 && totalWritten != length) { + IOException io = new IOException ("Cannot close the stream until all bytes are written"); + closed = true; + disposed = true; + var throwMe = new WebException ("Request was cancelled.", WebExceptionStatus.RequestCanceled, WebExceptionInternalStatus.RequestFatal, io); + Operation.CompleteRequestWritten (this, throwMe); + throw throwMe; + } + + // Commented out the next line to fix xamarin bug #1512 + //WriteRequest (); + disposed = true; + Operation.CompleteRequestWritten (this); + } + } +} diff --git a/mcs/class/System/System.Net/WebResponseStream.cs b/mcs/class/System/System.Net/WebResponseStream.cs new file mode 100644 index 000000000000..75ec4b7d6c3b --- /dev/null +++ b/mcs/class/System/System.Net/WebResponseStream.cs @@ -0,0 +1,733 @@ +// +// WebResponseStream.cs +// +// Author: +// Martin Baulig +// +// Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System.IO; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.ExceptionServices; +using System.Net.Sockets; + +namespace System.Net +{ + class WebResponseStream : WebConnectionStream + { + BufferOffsetSize readBuffer; + long contentLength; + long totalRead; + bool nextReadCalled; + int stream_length; // -1 when CL not present + TaskCompletionSource readTcs; + object locker = new object (); + int nestedRead; + bool read_eof; + + public WebRequestStream RequestStream { + get; + } + + public WebHeaderCollection Headers { + get; + private set; + } + + public HttpStatusCode StatusCode { + get; + private set; + } + + public string StatusDescription { + get; + private set; + } + + public Version Version { + get; + private set; + } + + public bool KeepAlive { + get; + private set; + } + + internal readonly string ME; + + public WebResponseStream (WebRequestStream request) + : base (request.Connection, request.Operation, request.InnerStream) + { + RequestStream = request; + request.InnerStream.ReadTimeout = ReadTimeout; + +#if MONO_WEB_DEBUG + ME = $"WRP(Cnc={Connection.ID}, Op={Operation.ID})"; +#endif + } + + public override long Length { + get { + return stream_length; + } + } + + public override bool CanRead => true; + + public override bool CanWrite => false; + + protected bool ChunkedRead { + get; + private set; + } + + protected MonoChunkStream ChunkStream { + get; + private set; + } + + public override async Task ReadAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + WebConnection.Debug ($"{ME} READ ASYNC"); + + cancellationToken.ThrowIfCancellationRequested (); + + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + int length = buffer.Length; + if (offset < 0 || length < offset) + throw new ArgumentOutOfRangeException (nameof (offset)); + if (size < 0 || (length - offset) < size) + throw new ArgumentOutOfRangeException (nameof (size)); + + if (Interlocked.CompareExchange (ref nestedRead, 1, 0) != 0) + throw new InvalidOperationException ("Invalid nested call."); + + var myReadTcs = new TaskCompletionSource (); + while (!cancellationToken.IsCancellationRequested) { + /* + * 'readTcs' is set by ReadAllAsync(). + */ + var oldReadTcs = Interlocked.CompareExchange (ref readTcs, myReadTcs, null); + WebConnection.Debug ($"{ME} READ ASYNC #1: {oldReadTcs != null}"); + if (oldReadTcs == null) + break; + await oldReadTcs.Task.ConfigureAwait (false); + } + + WebConnection.Debug ($"{ME} READ ASYNC #2: {totalRead} {contentLength}"); + + int oldBytes = 0, nbytes = 0; + Exception throwMe = null; + + try { + // FIXME: NetworkStream.ReadAsync() does not support cancellation. + (oldBytes, nbytes) = await HttpWebRequest.RunWithTimeout ( + ct => ProcessRead (buffer, offset, size, ct), + ReadTimeout, () => { + Operation.Abort (); + InnerStream.Dispose (); + }).ConfigureAwait (false); + } catch (Exception e) { + throwMe = GetReadException (WebExceptionStatus.ReceiveFailure, e, "ReadAsync"); + } + + WebConnection.Debug ($"{ME} READ ASYNC #3: {totalRead} {contentLength} - {oldBytes} {nbytes} {throwMe?.Message}"); + + if (throwMe != null) { + lock (locker) { + myReadTcs.TrySetException (throwMe); + readTcs = null; + nestedRead = 0; + } + + closed = true; + Operation.CompleteResponseRead (false, throwMe); + throw throwMe; + } + + lock (locker) { + readTcs.TrySetResult (oldBytes + nbytes); + readTcs = null; + nestedRead = 0; + } + + if (totalRead >= contentLength && !nextReadCalled) { + WebConnection.Debug ($"{ME} READ ASYNC - READ COMPLETE: {oldBytes} {nbytes} - {totalRead} {contentLength} {nextReadCalled}"); + if (!nextReadCalled) { + nextReadCalled = true; + Operation.CompleteResponseRead (true); + } + } + + return oldBytes + nbytes; + } + + async Task<(int, int)> ProcessRead (byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + WebConnection.Debug ($"{ME} PROCESS READ: {totalRead} {contentLength}"); + + cancellationToken.ThrowIfCancellationRequested (); + if (totalRead >= contentLength) { + read_eof = true; + contentLength = totalRead; + return (0, 0); + } + + int oldBytes = 0; + int remaining = readBuffer?.Size ?? 0; + if (remaining > 0) { + int copy = (remaining > size) ? size : remaining; + Buffer.BlockCopy (readBuffer.Buffer, readBuffer.Offset, buffer, offset, copy); + readBuffer.Offset += copy; + readBuffer.Size -= copy; + offset += copy; + size -= copy; + totalRead += copy; + if (totalRead >= contentLength) { + contentLength = totalRead; + read_eof = true; + } + if (size == 0 || totalRead >= contentLength) + return (0, copy); + oldBytes = copy; + } + + if (contentLength != Int64.MaxValue && contentLength - totalRead < size) + size = (int)(contentLength - totalRead); + + WebConnection.Debug ($"{ME} PROCESS READ #1: {oldBytes} {size} {read_eof}"); + + if (read_eof) { + contentLength = totalRead; + return (oldBytes, 0); + } + + var ret = await InnerReadAsync (buffer, offset, size, cancellationToken).ConfigureAwait (false); + + if (ret <= 0) { + read_eof = true; + contentLength = totalRead; + return (oldBytes, 0); + } + + totalRead += ret; + return (oldBytes, ret); + } + + internal async Task InnerReadAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + WebConnection.Debug ($"{ME} INNER READ ASYNC"); + + Operation.ThrowIfDisposed (cancellationToken); + + int nbytes = 0; + bool done = false; + + if (!ChunkedRead || (!ChunkStream.DataAvailable && ChunkStream.WantMore)) { + nbytes = await InnerStream.ReadAsync (buffer, offset, size, cancellationToken).ConfigureAwait (false); + WebConnection.Debug ($"{ME} INNER READ ASYNC #1: {nbytes} {ChunkedRead}"); + if (!ChunkedRead) + return nbytes; + done = nbytes == 0; + } + + try { + ChunkStream.WriteAndReadBack (buffer, offset, size, ref nbytes); + WebConnection.Debug ($"{ME} INNER READ ASYNC #1: {done} {nbytes} {ChunkStream.WantMore}"); + if (!done && nbytes == 0 && ChunkStream.WantMore) + nbytes = await EnsureReadAsync (buffer, offset, size, cancellationToken).ConfigureAwait (false); + } catch (Exception e) { + if (e is WebException || e is OperationCanceledException) + throw; + throw new WebException ("Invalid chunked data.", e, WebExceptionStatus.ServerProtocolViolation, null); + } + + if ((done || nbytes == 0) && ChunkStream.ChunkLeft != 0) { + // HandleError (WebExceptionStatus.ReceiveFailure, null, "chunked EndRead"); + throw new WebException ("Read error", null, WebExceptionStatus.ReceiveFailure, null); + } + + return nbytes; + } + + async Task EnsureReadAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + byte[] morebytes = null; + int nbytes = 0; + while (nbytes == 0 && ChunkStream.WantMore && !cancellationToken.IsCancellationRequested) { + int localsize = ChunkStream.ChunkLeft; + if (localsize <= 0) // not read chunk size yet + localsize = 1024; + else if (localsize > 16384) + localsize = 16384; + + if (morebytes == null || morebytes.Length < localsize) + morebytes = new byte[localsize]; + + int nread = await InnerStream.ReadAsync (morebytes, 0, localsize, cancellationToken).ConfigureAwait (false); + if (nread <= 0) + return 0; // Error + + ChunkStream.Write (morebytes, 0, nread); + nbytes += ChunkStream.Read (buffer, offset + nbytes, size - nbytes); + } + + return nbytes; + } + + bool CheckAuthHeader (string headerName) + { + var authHeader = Headers[headerName]; + return (authHeader != null && authHeader.IndexOf ("NTLM", StringComparison.Ordinal) != -1); + } + + bool IsNtlmAuth () + { + bool isProxy = (Request.Proxy != null && !Request.Proxy.IsBypassed (Request.Address)); + if (isProxy && CheckAuthHeader ("Proxy-Authenticate")) + return true; + return CheckAuthHeader ("WWW-Authenticate"); + } + + bool ExpectContent { + get { + if (Request.Method == "HEAD") + return false; + return ((int)StatusCode >= 200 && (int)StatusCode != 204 && (int)StatusCode != 304); + } + } + + async Task Initialize (BufferOffsetSize buffer, CancellationToken cancellationToken) + { + WebConnection.Debug ($"{ME} INIT: status={(int)StatusCode} bos={buffer.Offset}/{buffer.Size}"); + + string contentType = Headers["Transfer-Encoding"]; + bool chunkedRead = (contentType != null && contentType.IndexOf ("chunked", StringComparison.OrdinalIgnoreCase) != -1); + string clength = Headers["Content-Length"]; + if (!chunkedRead && !string.IsNullOrEmpty (clength)) { + if (!long.TryParse (clength, out contentLength)) + contentLength = Int64.MaxValue; + } else { + contentLength = Int64.MaxValue; + } + + if (Version == HttpVersion.Version11 && RequestStream.KeepAlive) { + KeepAlive = true; + var cncHeader = Headers[ServicePoint.UsesProxy ? "Proxy-Connection" : "Connection"]; + if (cncHeader != null) { + cncHeader = cncHeader.ToLower (); + KeepAlive = cncHeader.IndexOf ("keep-alive", StringComparison.Ordinal) != -1; + if (cncHeader.IndexOf ("close", StringComparison.Ordinal) != -1) + KeepAlive = false; + } + } + + // Negative numbers? + if (!Int32.TryParse (clength, out stream_length)) + stream_length = -1; + + string me = "WebResponseStream.Initialize()"; + string tencoding = null; + if (ExpectContent) + tencoding = Headers["Transfer-Encoding"]; + + ChunkedRead = (tencoding != null && tencoding.IndexOf ("chunked", StringComparison.OrdinalIgnoreCase) != -1); + if (!ChunkedRead) { + readBuffer = buffer; + try { + if (contentLength > 0 && readBuffer.Size >= contentLength) { + if (!IsNtlmAuth ()) + await ReadAllAsync (false, cancellationToken).ConfigureAwait (false); + } + } catch (Exception e) { + throw GetReadException (WebExceptionStatus.ReceiveFailure, e, me); + } + } else if (ChunkStream == null) { + try { + ChunkStream = new MonoChunkStream (buffer.Buffer, buffer.Offset, buffer.Offset + buffer.Size, Headers); + } catch (Exception e) { + throw GetReadException (WebExceptionStatus.ServerProtocolViolation, e, me); + } + } else { + ChunkStream.ResetBuffer (); + try { + ChunkStream.Write (buffer.Buffer, buffer.Offset, buffer.Size); + } catch (Exception e) { + throw GetReadException (WebExceptionStatus.ServerProtocolViolation, e, me); + } + } + + WebConnection.Debug ($"{ME} INIT #1: - {ExpectContent} {closed} {nextReadCalled}"); + + if (!ExpectContent) { + if (!closed && !nextReadCalled) { + if (contentLength == Int64.MaxValue) + contentLength = 0; + nextReadCalled = true; + } + Operation.CompleteResponseRead (true); + } + } + + internal async Task ReadAllAsync (bool resending, CancellationToken cancellationToken) + { + WebConnection.Debug ($"{ME} READ ALL ASYNC: resending={resending} eof={read_eof} total={totalRead} " + + "length={contentLength} nextReadCalled={nextReadCalled}"); + if (read_eof || totalRead >= contentLength || nextReadCalled) { + if (!nextReadCalled) { + nextReadCalled = true; + Operation.CompleteResponseRead (true); + } + return; + } + + var timeoutTask = Task.Delay (ReadTimeout); + var myReadTcs = new TaskCompletionSource (); + while (true) { + /* + * 'readTcs' is set by ReadAsync(). + */ + cancellationToken.ThrowIfCancellationRequested (); + var oldReadTcs = Interlocked.CompareExchange (ref readTcs, myReadTcs, null); + if (oldReadTcs == null) + break; + + // ReadAsync() is in progress. + var anyTask = await Task.WhenAny (oldReadTcs.Task, timeoutTask).ConfigureAwait (false); + if (anyTask == timeoutTask) + throw new WebException ("The operation has timed out.", WebExceptionStatus.Timeout); + } + + WebConnection.Debug ($"{ME} READ ALL ASYNC #1"); + + cancellationToken.ThrowIfCancellationRequested (); + + try { + if (totalRead >= contentLength) + return; + + byte[] b = null; + int new_size; + + if (contentLength == Int64.MaxValue && !ChunkedRead) { + WebConnection.Debug ($"{ME} READ ALL ASYNC - NEITHER LENGTH NOR CHUNKED"); + /* + * This is a violation of the HTTP Spec - the server neither send a + * "Content-Length:" nor a "Transfer-Encoding: chunked" header. + * + * When we're redirecting or resending for NTLM, then we can simply close + * the connection here. + * + * However, if it's the final reply, then we need to try our best to read it. + */ + if (resending) { + Close (); + return; + } + KeepAlive = false; + } + + if (contentLength == Int64.MaxValue) { + MemoryStream ms = new MemoryStream (); + BufferOffsetSize buffer = null; + if (readBuffer != null && readBuffer.Size > 0) { + ms.Write (readBuffer.Buffer, readBuffer.Offset, readBuffer.Size); + readBuffer.Offset = 0; + readBuffer.Size = readBuffer.Buffer.Length; + if (readBuffer.Buffer.Length >= 8192) + buffer = readBuffer; + } + + if (buffer == null) + buffer = new BufferOffsetSize (new byte[8192], false); + + int read; + while ((read = await InnerReadAsync (buffer.Buffer, buffer.Offset, buffer.Size, cancellationToken)) != 0) + ms.Write (buffer.Buffer, buffer.Offset, read); + + new_size = (int)ms.Length; + contentLength = new_size; + readBuffer = new BufferOffsetSize (ms.GetBuffer (), 0, new_size, false); + } else { + new_size = (int)(contentLength - totalRead); + b = new byte[new_size]; + int readSize = 0; + if (readBuffer != null && readBuffer.Size > 0) { + readSize = readBuffer.Size; + if (readSize > new_size) + readSize = new_size; + + Buffer.BlockCopy (readBuffer.Buffer, readBuffer.Offset, b, 0, readSize); + } + + int remaining = new_size - readSize; + int r = -1; + while (remaining > 0 && r != 0) { + r = await InnerReadAsync (b, readSize, remaining, cancellationToken); + remaining -= r; + readSize += r; + } + } + + readBuffer = new BufferOffsetSize (b, 0, new_size, false); + totalRead = 0; + nextReadCalled = true; + myReadTcs.TrySetResult (new_size); + } catch (Exception ex) { + WebConnection.Debug ($"{ME} READ ALL ASYNC EX: {ex.Message}"); + myReadTcs.TrySetException (ex); + throw; + } finally { + WebConnection.Debug ($"{ME} READ ALL ASYNC #2"); + readTcs = null; + } + + Operation.CompleteResponseRead (true); + } + + public override Task WriteAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken) + { + return Task.FromException (new NotSupportedException (SR.net_readonlystream)); + } + + protected override void Close_internal (ref bool disposed) + { + WebConnection.Debug ($"{ME} CLOSE: {disposed} {closed} {nextReadCalled}"); + if (!closed && !nextReadCalled) { + nextReadCalled = true; + if (totalRead >= contentLength) { + disposed = true; + Operation.CompleteResponseRead (true); + } else { + // If we have not read all the contents + closed = true; + disposed = true; + Operation.CompleteResponseRead (false); + } + } + } + + WebException GetReadException (WebExceptionStatus status, Exception error, string where) + { + error = GetException (error); + string msg = $"Error getting response stream ({where}): {status}"; + if (error == null) + return new WebException ($"Error getting response stream ({where}): {status}", status); + if (error is WebException wexc) + return wexc; + if (Operation.Aborted || error is OperationCanceledException || error is ObjectDisposedException) + return HttpWebRequest.CreateRequestAbortedException (); + return new WebException ($"Error getting response stream ({where}): {status} {error.Message}", status, + WebExceptionInternalStatus.RequestFatal, error); + } + + internal async Task InitReadAsync (CancellationToken cancellationToken) + { + WebConnection.Debug ($"{ME} INIT READ ASYNC"); + + var buffer = new BufferOffsetSize (new byte[4096], false); + var state = ReadState.None; + int position = 0; + + while (true) { + Operation.ThrowIfClosedOrDisposed (cancellationToken); + + WebConnection.Debug ($"{ME} INIT READ ASYNC LOOP: {state} {position} - {buffer.Offset}/{buffer.Size}"); + + var nread = await InnerStream.ReadAsync ( + buffer.Buffer, buffer.Offset, buffer.Size, cancellationToken).ConfigureAwait (false); + + WebConnection.Debug ($"{ME} INIT READ ASYNC LOOP #1: {state} {position} - {buffer.Offset}/{buffer.Size} - {nread}"); + + if (nread == 0) + throw GetReadException (WebExceptionStatus.ReceiveFailure, null, "ReadDoneAsync2"); + + if (nread < 0) + throw GetReadException (WebExceptionStatus.ServerProtocolViolation, null, "ReadDoneAsync3"); + + buffer.Offset += nread; + buffer.Size -= nread; + + if (state == ReadState.None) { + try { + var oldPos = position; + if (!GetResponse (buffer, ref position, ref state)) + position = oldPos; + } catch (Exception e) { + WebConnection.Debug ($"{ME} INIT READ ASYNC FAILED: {e.Message}\n{e}"); + throw GetReadException (WebExceptionStatus.ServerProtocolViolation, e, "ReadDoneAsync4"); + } + } + + if (state == ReadState.Aborted) + throw GetReadException (WebExceptionStatus.RequestCanceled, null, "ReadDoneAsync5"); + + if (state == ReadState.Content) { + buffer.Size = buffer.Offset - position; + buffer.Offset = position; + break; + } + + int est = nread * 2; + if (est > buffer.Size) { + var newBuffer = new byte [buffer.Buffer.Length + est]; + Buffer.BlockCopy (buffer.Buffer, 0, newBuffer, 0, buffer.Offset); + buffer = new BufferOffsetSize (newBuffer, buffer.Offset, newBuffer.Length - buffer.Offset, false); + } + state = ReadState.None; + position = 0; + } + + WebConnection.Debug ($"{ME} INIT READ ASYNC LOOP DONE: {buffer.Offset} {buffer.Size}"); + + try { + Operation.ThrowIfDisposed (cancellationToken); + await Initialize (buffer, cancellationToken).ConfigureAwait (false); + } catch (Exception e) { + throw GetReadException (WebExceptionStatus.ReceiveFailure, e, "ReadDoneAsync6"); + } + } + + bool GetResponse (BufferOffsetSize buffer, ref int pos, ref ReadState state) + { + string line = null; + bool lineok = false; + bool isContinue = false; + bool emptyFirstLine = false; + do { + if (state == ReadState.Aborted) + throw GetReadException (WebExceptionStatus.RequestCanceled, null, "GetResponse"); + + if (state == ReadState.None) { + lineok = WebConnection.ReadLine (buffer.Buffer, ref pos, buffer.Offset, ref line); + if (!lineok) + return false; + + if (line == null) { + emptyFirstLine = true; + continue; + } + emptyFirstLine = false; + state = ReadState.Status; + + string[] parts = line.Split (' '); + if (parts.Length < 2) + throw GetReadException (WebExceptionStatus.ServerProtocolViolation, null, "GetResponse"); + + if (String.Compare (parts[0], "HTTP/1.1", true) == 0) { + Version = HttpVersion.Version11; + ServicePoint.SetVersion (HttpVersion.Version11); + } else { + Version = HttpVersion.Version10; + ServicePoint.SetVersion (HttpVersion.Version10); + } + + StatusCode = (HttpStatusCode)UInt32.Parse (parts[1]); + if (parts.Length >= 3) + StatusDescription = String.Join (" ", parts, 2, parts.Length - 2); + else + StatusDescription = string.Empty; + + if (pos >= buffer.Size) + return true; + } + + emptyFirstLine = false; + if (state == ReadState.Status) { + state = ReadState.Headers; + Headers = new WebHeaderCollection (); + var headerList = new List (); + bool finished = false; + while (!finished) { + if (WebConnection.ReadLine (buffer.Buffer, ref pos, buffer.Offset, ref line) == false) + break; + + if (line == null) { + // Empty line: end of headers + finished = true; + continue; + } + + if (line.Length > 0 && (line[0] == ' ' || line[0] == '\t')) { + int count = headerList.Count - 1; + if (count < 0) + break; + + string prev = headerList[count] + line; + headerList[count] = prev; + } else { + headerList.Add (line); + } + } + + if (!finished) + return false; + + // .NET uses ParseHeaders or ParseHeadersStrict which is much better + foreach (string s in headerList) { + + int pos_s = s.IndexOf (':'); + if (pos_s == -1) + throw new ArgumentException ("no colon found", "header"); + + var header = s.Substring (0, pos_s); + var value = s.Substring (pos_s + 1).Trim (); + + if (WebHeaderCollection.AllowMultiValues (header)) { + Headers.AddInternal (header, value); + } else { + Headers.SetInternal (header, value); + } + } + + if (StatusCode == HttpStatusCode.Continue) { + ServicePoint.SendContinue = true; + if (pos >= buffer.Offset) + return true; + + if (Request.ExpectContinue) { + Request.DoContinueDelegate ((int)StatusCode, Headers); + // Prevent double calls when getting the + // headers in several packets. + Request.ExpectContinue = false; + } + + state = ReadState.None; + isContinue = true; + } else { + state = ReadState.Content; + return true; + } + } + } while (emptyFirstLine || isContinue); + + throw GetReadException (WebExceptionStatus.ServerProtocolViolation, null, "GetResponse"); + } + + + } +} diff --git a/mcs/class/System/Test/System.Net/HttpWebRequestTest.cs b/mcs/class/System/Test/System.Net/HttpWebRequestTest.cs index 4cabb0ca84ea..bdcde4afe539 100644 --- a/mcs/class/System/Test/System.Net/HttpWebRequestTest.cs +++ b/mcs/class/System/Test/System.Net/HttpWebRequestTest.cs @@ -303,7 +303,8 @@ public void BeginGetRequestStream_Body_NotAllowed () request.Method = "GET"; try { - request.BeginGetRequestStream (null, null); + var result = request.BeginGetRequestStream (null, null); + request.EndGetRequestStream (result); Assert.Fail ("#A1"); } catch (ProtocolViolationException ex) { // Cannot send a content-body with this @@ -316,7 +317,8 @@ public void BeginGetRequestStream_Body_NotAllowed () request.Method = "HEAD"; try { - request.BeginGetRequestStream (null, null); + var res = request.BeginGetRequestStream (null, null); + request.EndGetRequestStream (res); Assert.Fail ("#B1"); } catch (ProtocolViolationException ex) { // Cannot send a content-body with this @@ -358,7 +360,8 @@ public void BeginGetRequestStream_NoBuffering () req.AllowWriteStreamBuffering = false; try { - req.BeginGetRequestStream (null, null); + var result = req.BeginGetRequestStream (null, null); + req.EndGetRequestStream (result); Assert.Fail ("#A1"); } catch (ProtocolViolationException ex) { // When performing a write operation with @@ -3076,7 +3079,8 @@ public void CanWrite () try { Assert.IsTrue (rs.CanWrite, "#1"); rs.Close (); - Assert.IsFalse (rs.CanWrite, "#2"); + // CanRead and CanWrite do not change status after closing. + Assert.IsTrue (rs.CanWrite, "#2"); } finally { rs.Close (); req.Abort (); diff --git a/mcs/class/System/common.sources b/mcs/class/System/common.sources index 29ca5d4b9cfb..f5c65409044b 100644 --- a/mcs/class/System/common.sources +++ b/mcs/class/System/common.sources @@ -59,10 +59,8 @@ System.Net/ListenerPrefix.cs System.Net/MonoHttpDate.cs System.Net/NetConfig.cs System.Net/RequestStream.cs -System.Net/SimpleAsyncResult.cs System.Net/SocketPermissionAttribute.cs System.Net/SocketPermission.cs -System.Net/WebAsyncResult.cs System.Net.Mail/AlternateView.cs System.Net.Mail/AlternateViewCollection.cs diff --git a/mcs/class/System/common_networking.sources b/mcs/class/System/common_networking.sources index f161a1657e9e..3767fc1c1044 100644 --- a/mcs/class/System/common_networking.sources +++ b/mcs/class/System/common_networking.sources @@ -37,16 +37,18 @@ System.Net/HttpListenerResponse.cs System.Net/HttpListenerTimeoutManager.cs System.Net/HttpWebRequest.cs System.Net/HttpWebResponse.cs -System.Net/IWebConnectionState.cs System.Net/ListenerAsyncResult.cs System.Net/ResponseStream.cs System.Net/ServicePoint.cs System.Net/ServicePointManager.cs System.Net/ServicePointManager.extra.cs +System.Net/ServicePointScheduler.cs System.Net/WebConnection.cs -System.Net/WebConnectionData.cs -System.Net/WebConnectionGroup.cs System.Net/WebConnectionStream.cs +System.Net/WebConnectionTunnel.cs +System.Net/WebOperation.cs +System.Net/WebRequestStream.cs +System.Net/WebResponseStream.cs ../referencesource/System/net/System/Net/Sockets/TCPClient.cs ../referencesource/System/net/System/Net/Sockets/TCPListener.cs diff --git a/mcs/class/referencesource/System/net/System/Net/webclient.cs b/mcs/class/referencesource/System/net/System/Net/webclient.cs index 96cf1c91007b..3b6da7ced091 100644 --- a/mcs/class/referencesource/System/net/System/Net/webclient.cs +++ b/mcs/class/referencesource/System/net/System/Net/webclient.cs @@ -1805,20 +1805,9 @@ private void OpenReadOperationCompleted(object arg) { OnOpenReadCompleted((OpenReadCompletedEventArgs)arg); } private void OpenReadAsyncCallback(IAsyncResult result) { -#if MONO - // It can be removed when we are full referencesource - AsyncOperation asyncOp = (AsyncOperation) result.AsyncState; - WebRequest request; - if (result is WebAsyncResult) { - request = (WebRequest) ((WebAsyncResult) result).AsyncObject; - } else { - request = (WebRequest) ((LazyAsyncResult) result).AsyncObject; - } -#else - LazyAsyncResult lazyAsyncResult = (LazyAsyncResult) result; - AsyncOperation asyncOp = (AsyncOperation) lazyAsyncResult.AsyncState; - WebRequest request = (WebRequest) lazyAsyncResult.AsyncObject; -#endif + Tuple userData = (Tuple)result.AsyncState; + WebRequest request = userData.Item1; + AsyncOperation asyncOp = userData.Item2; Stream stream = null; Exception exception = null; try { @@ -1857,7 +1846,7 @@ public void OpenReadAsync(Uri address, object userToken) m_AsyncOp = asyncOp; try { WebRequest request = m_WebRequest = GetWebRequest(GetUri(address)); - request.BeginGetResponse(new AsyncCallback(OpenReadAsyncCallback), asyncOp); + request.BeginGetResponse(new AsyncCallback(OpenReadAsyncCallback), new Tuple(request, asyncOp)); } catch (Exception e) { if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException) { throw; @@ -1887,13 +1876,9 @@ private void OpenWriteOperationCompleted(object arg) { OnOpenWriteCompleted((OpenWriteCompletedEventArgs)arg); } private void OpenWriteAsyncCallback(IAsyncResult result) { -#if MONO - var lazyAsyncResult = (WebAsyncResult) result; -#else - LazyAsyncResult lazyAsyncResult = (LazyAsyncResult) result; -#endif - AsyncOperation asyncOp = (AsyncOperation) lazyAsyncResult.AsyncState; - WebRequest request = (WebRequest) lazyAsyncResult.AsyncObject; + Tuple userData = (Tuple)result.AsyncState; + WebRequest request = userData.Item1; + AsyncOperation asyncOp = userData.Item2; WebClientWriteStream stream = null; Exception exception = null; @@ -1943,7 +1928,7 @@ public void OpenWriteAsync(Uri address, string method, object userToken) try { m_Method = method; WebRequest request = m_WebRequest = GetWebRequest(GetUri(address)); - request.BeginGetRequestStream(new AsyncCallback(OpenWriteAsyncCallback), asyncOp); + request.BeginGetRequestStream(new AsyncCallback(OpenWriteAsyncCallback), new Tuple(request, asyncOp)); } catch (Exception e) { if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException) { throw;