using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.IO; using System.Threading; namespace Duplicati.Library.Utility { /// /// This class wraps a HttpWebRequest and performs GetRequestStream and GetResponseStream /// with async methods while maintaining a synchronous interface /// public class AsyncHttpRequest { /// /// The method being wrapped /// private readonly WebRequest m_request; /// /// The current internal state of the object /// private RequestStates m_state = RequestStates.Created; /// /// The request async wrapper /// private AsyncWrapper m_asyncRequest = null; /// /// The response async wrapper /// private AsyncWrapper m_asyncResponse = null; /// /// The request/response timeout value /// private int m_timeout = 100000; /// /// The activity timeout value /// private readonly int m_activity_timeout = (int)TimeSpan.FromSeconds(30).TotalMilliseconds; /// /// List of valid states /// private enum RequestStates { /// /// The request has been created /// Created, /// /// The request stream has been requested /// GetRequest, /// /// The response has been requested /// GetResponse, /// /// /// Done } /// /// Constructs a new request from a url /// /// The url to create the request from public AsyncHttpRequest(string url) : this(System.Net.WebRequest.Create(url)) { } /// /// Creates a async request wrapper for an existing url /// /// The request to wrap public AsyncHttpRequest(WebRequest request) { if (request == null) throw new ArgumentNullException(nameof(request)); m_request = request; m_timeout = m_request.Timeout; if (m_timeout != System.Threading.Timeout.Infinite) { var tmp = (int)HttpContextSettings.OperationTimeout.TotalMilliseconds; if (tmp <= 0) m_timeout = System.Threading.Timeout.Infinite; else m_timeout = Math.Max(m_timeout, tmp); } m_activity_timeout = (int)HttpContextSettings.ReadWriteTimeout.TotalMilliseconds; if (m_activity_timeout <= 0) m_activity_timeout = System.Threading.Timeout.Infinite; //We set this to prevent timeout related stuff from happening outside this module m_request.Timeout = System.Threading.Timeout.Infinite; //Then we register custom settings if (m_request is HttpWebRequest) { if (((HttpWebRequest)m_request).ReadWriteTimeout != System.Threading.Timeout.Infinite) m_activity_timeout = ((HttpWebRequest)m_request).ReadWriteTimeout; ((HttpWebRequest)m_request).ReadWriteTimeout = System.Threading.Timeout.Infinite; // Prevent in-memory buffering causing out-of-memory issues ((HttpWebRequest)m_request).AllowReadStreamBuffering = HttpContextSettings.BufferRequests; } } /// /// Gets the request that is wrapped /// public WebRequest Request { get { return m_request; } } /// /// Gets or sets the timeout used to guard the and calls /// public int Timeout { get { return m_timeout; } set { m_timeout = value; } } /// /// Gets the request stream /// /// The request stream /// The content length to use public Stream GetRequestStream(long contentlength = -1) { // Prevent in-memory buffering causing out-of-memory issues if (m_request is HttpWebRequest) { if (contentlength >= 0) ((HttpWebRequest)m_request).ContentLength = contentlength; if (m_request.ContentLength >= 0) ((HttpWebRequest)m_request).AllowWriteStreamBuffering = false; } if (m_state == RequestStates.GetRequest) return (Stream)m_asyncRequest.GetResponseOrStream(); if (m_state != RequestStates.Created) throw new InvalidOperationException(); m_asyncRequest = new AsyncWrapper(this, true); m_state = RequestStates.GetRequest; return TrySetTimeout((Stream)m_asyncRequest.GetResponseOrStream(), m_activity_timeout); } /// /// Gets the response object /// /// The web response public WebResponse GetResponse() { if (m_state == RequestStates.GetResponse) return (WebResponse)m_asyncResponse.GetResponseOrStream(); if (m_state == RequestStates.Done) throw new InvalidOperationException(); m_asyncRequest = null; m_asyncResponse = new AsyncWrapper(this, false); m_state = RequestStates.GetResponse; return (WebResponse)m_asyncResponse.GetResponseOrStream(); } public Stream GetResponseStream() { return TrySetTimeout(GetResponse().GetResponseStream(), m_activity_timeout); } public static Stream TrySetTimeout(Stream str, int timeoutmilliseconds = 30000) { try { str.ReadTimeout = timeoutmilliseconds; } catch { } return str; } /// /// Wrapper class for getting request and respone objects in a async manner /// private class AsyncWrapper { private readonly IAsyncResult m_async = null; private Stream m_stream = null; private WebResponse m_response = null; private readonly AsyncHttpRequest m_owner; private Exception m_exception = null; private readonly ManualResetEvent m_event = new ManualResetEvent(false); private readonly bool m_isRequest; private bool m_timedout = false; public AsyncWrapper(AsyncHttpRequest owner, bool isRequest) { m_owner = owner; m_isRequest = isRequest; if (m_isRequest) m_async = m_owner.m_request.BeginGetRequestStream(new AsyncCallback(this.OnAsync), null); else m_async = m_owner.m_request.BeginGetResponse(new AsyncCallback(this.OnAsync), null); if ( m_owner.m_timeout != System.Threading.Timeout.Infinite) ThreadPool.RegisterWaitForSingleObject(m_async.AsyncWaitHandle, new WaitOrTimerCallback(this.OnTimeout), null, TimeSpan.FromMilliseconds( m_owner.m_timeout), true); } private void OnAsync(IAsyncResult r) { try { if (m_isRequest) m_stream = m_owner.m_request.EndGetRequestStream(r); else m_response = m_owner.m_request.EndGetResponse(r); } catch (Exception ex) { if (m_timedout) m_exception = new WebException(string.Format("{0} timed out", m_isRequest ? "GetRequestStream" : "GetResponse"), ex, WebExceptionStatus.Timeout, ex is WebException ? ((WebException)ex).Response : null); else { // Workaround for: https://bugzilla.xamarin.com/show_bug.cgi?id=28287 var wex = ex; if (ex is WebException && ((WebException)ex).Response == null) { WebResponse resp = null; try { resp = (WebResponse)r.GetType().GetProperty("Response", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(r); } catch {} if (resp == null) try { resp = (WebResponse)m_owner.m_request.GetType().GetField("webResponse", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(m_owner.m_request); } catch { } if (resp != null) wex = new WebException(ex.Message, ex.InnerException, ((WebException)ex).Status, resp); } m_exception = wex; } } finally { m_event.Set(); } } private void OnTimeout(object state, bool timedout) { if (timedout) { if (!m_event.WaitOne(0, false)) { m_timedout = true; m_owner.m_request.Abort(); } } } public object GetResponseOrStream() { try { m_event.WaitOne(); } catch (ThreadAbortException) { m_owner.m_request.Abort(); //Grant a little time for cleanups m_event.WaitOne((int)TimeSpan.FromSeconds(5).TotalMilliseconds, false); //The abort exception will automatically be rethrown } if (m_exception != null) throw m_exception; if (m_isRequest) return m_stream; else return m_response; } } } }