using System; using System.IO; using System.Net; using System.Text; using System.Threading; using ServiceStack.Logging; using ServiceStack.ServiceHost; using ServiceStack.Text; using ServiceStack.Common.Web; namespace ServiceStack.ServiceClient.Web { /** * Need to provide async request options * http://msdn.microsoft.com/en-us/library/86wf6409(VS.71).aspx */ public class AsyncServiceClient { private static readonly ILog Log = LogManager.GetLogger(typeof(AsyncServiceClient)); private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); private HttpWebRequest _webRequest = null; /// /// The request filter is called before any request. /// This request filter is executed globally. /// public static Action HttpWebRequestFilter { get; set; } /// /// The response action is called once the server response is available. /// It will allow you to access raw response information. /// This response action is executed globally. /// Note that you should NOT consume the response stream as this is handled by ServiceStack /// public static Action HttpWebResponseFilter { get; set; } /// /// Called before request resend, when the initial request required authentication /// public Action OnAuthenticationRequired { get; set; } const int BufferSize = 4096; public ICredentials Credentials { get; set; } public bool StoreCookies { get; set; } public CookieContainer CookieContainer { get; set; } /// /// The request filter is called before any request. /// This request filter only works with the instance where it was set (not global). /// public Action LocalHttpWebRequestFilter { get; set; } /// /// The response action is called once the server response is available. /// It will allow you to access raw response information. /// Note that you should NOT consume the response stream as this is handled by ServiceStack /// public Action LocalHttpWebResponseFilter { get; set; } public string BaseUri { get; set; } internal class RequestState : IDisposable { private bool _timedOut; // Pass the correct error back even on Async Calls public RequestState() { BufferRead = new byte[BufferSize]; TextData = new StringBuilder(); BytesData = new MemoryStream(BufferSize); WebRequest = null; ResponseStream = null; } public string HttpMethod; public string Url; public StringBuilder TextData; public MemoryStream BytesData; public byte[] BufferRead; public object Request; public HttpWebRequest WebRequest; public HttpWebResponse WebResponse; public Stream ResponseStream; public int Completed; public int RequestCount; public Timer Timer; public Action OnSuccess; public Action OnError; #if SILVERLIGHT public bool HandleCallbackOnUIThread { get; set; } #endif public void HandleSuccess(TResponse response) { if (this.OnSuccess == null) return; #if SILVERLIGHT if (this.HandleCallbackOnUIThread) System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() => this.OnSuccess(response)); else this.OnSuccess(response); #else this.OnSuccess(response); #endif } public void HandleError(TResponse response, Exception ex) { if (this.OnError == null) return; Exception toReturn = ex; if (_timedOut) { #if SILVERLIGHT WebException we = new WebException("The request timed out", ex, WebExceptionStatus.RequestCanceled, null); #else WebException we = new WebException("The request timed out", ex, WebExceptionStatus.Timeout, null); #endif toReturn = we; } #if SILVERLIGHT if (this.HandleCallbackOnUIThread) System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() => this.OnError(response, toReturn)); else this.OnError(response, toReturn); #else OnError(response, toReturn); #endif } public void StartTimer(TimeSpan timeOut) { this.Timer = new Timer(this.TimedOut, this, (int)timeOut.TotalMilliseconds, System.Threading.Timeout.Infinite); } public void TimedOut(object state) { if (Interlocked.Increment(ref Completed) == 1) { if (this.WebRequest != null) { _timedOut = true; this.WebRequest.Abort(); } } this.Timer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); this.Timer.Dispose(); this.Dispose(); } public void Dispose() { if (this.BytesData == null) return; this.BytesData.Dispose(); this.BytesData = null; } } public bool DisableAutoCompression { get; set; } public string UserName { get; set; } public string Password { get; set; } public void SetCredentials(string userName, string password) { this.UserName = userName; this.Password = password; } public TimeSpan? Timeout { get; set; } public string ContentType { get; set; } public StreamSerializerDelegate StreamSerializer { get; set; } public StreamDeserializerDelegate StreamDeserializer { get; set; } #if SILVERLIGHT public bool HandleCallbackOnUIThread { get; set; } public bool UseBrowserHttpHandling { get; set; } public bool ShareCookiesWithBrowser { get; set; } #endif public void SendAsync(string httpMethod, string absoluteUrl, object request, Action onSuccess, Action onError) { SendWebRequest(httpMethod, absoluteUrl, request, onSuccess, onError); } public void CancelAsync() { if (_webRequest != null) { // Request will be nulled after it throws an exception on its async methods // See - http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.abort _webRequest.Abort(); } } #if !SILVERLIGHT internal static void AllowAutoCompression(HttpWebRequest webRequest) { webRequest.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); webRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; } #endif private RequestState SendWebRequest(string httpMethod, string absoluteUrl, object request, Action onSuccess, Action onError) { if (httpMethod == null) throw new ArgumentNullException("httpMethod"); var requestUri = absoluteUrl; var httpGetOrDelete = (httpMethod == "GET" || httpMethod == "DELETE"); var hasQueryString = request != null && httpGetOrDelete; if (hasQueryString) { var queryString = QueryStringSerializer.SerializeToString(request); if (!string.IsNullOrEmpty(queryString)) { requestUri += "?" + queryString; } } #if SILVERLIGHT var creator = this.UseBrowserHttpHandling ? System.Net.Browser.WebRequestCreator.BrowserHttp : System.Net.Browser.WebRequestCreator.ClientHttp; var webRequest = (HttpWebRequest) creator.Create(new Uri(requestUri)); if (StoreCookies && !UseBrowserHttpHandling) { if (ShareCookiesWithBrowser) { if (CookieContainer == null) CookieContainer = new CookieContainer(); CookieContainer.SetCookies(new Uri(BaseUri), System.Windows.Browser.HtmlPage.Document.Cookies); } webRequest.CookieContainer = CookieContainer; } #else _webRequest = (HttpWebRequest)WebRequest.Create(requestUri); if (StoreCookies) { _webRequest.CookieContainer = CookieContainer; } #endif #if !SILVERLIGHT if (!DisableAutoCompression) { _webRequest.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); _webRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; } #endif var requestState = new RequestState { HttpMethod = httpMethod, Url = requestUri, #if SILVERLIGHT WebRequest = webRequest, #else WebRequest = _webRequest, #endif Request = request, OnSuccess = onSuccess, OnError = onError, #if SILVERLIGHT HandleCallbackOnUIThread = HandleCallbackOnUIThread, #endif }; requestState.StartTimer(this.Timeout.GetValueOrDefault(DefaultTimeout)); #if SILVERLIGHT SendWebRequestAsync(httpMethod, request, requestState, webRequest); #else SendWebRequestAsync(httpMethod, request, requestState, _webRequest); #endif return requestState; } private void SendWebRequestAsync(string httpMethod, object request, RequestState requestState, HttpWebRequest webRequest) { var httpGetOrDelete = (httpMethod == "GET" || httpMethod == "DELETE"); webRequest.Accept = string.Format("{0}, */*", ContentType); #if !SILVERLIGHT webRequest.Method = httpMethod; #else //Methods others than GET and POST are only supported by Client request creator, see //http://msdn.microsoft.com/en-us/library/cc838250(v=vs.95).aspx if (this.UseBrowserHttpHandling && httpMethod != "GET" && httpMethod != "POST") { webRequest.Method = "POST"; webRequest.Headers[HttpHeaders.XHttpMethodOverride] = httpMethod; } else { webRequest.Method = httpMethod; } #endif if (this.Credentials != null) { webRequest.Credentials = this.Credentials; } ApplyWebRequestFilters(webRequest); try { if (!httpGetOrDelete && request != null) { webRequest.ContentType = ContentType; webRequest.BeginGetRequestStream(RequestCallback, requestState); } else { requestState.WebRequest.BeginGetResponse(ResponseCallback, requestState); } } catch (Exception ex) { // BeginGetRequestStream can throw if request was aborted HandleResponseError(ex, requestState); } } private void RequestCallback(IAsyncResult asyncResult) { var requestState = (RequestState)asyncResult.AsyncState; try { var req = requestState.WebRequest; var postStream = req.EndGetRequestStream(asyncResult); StreamSerializer(null, requestState.Request, postStream); postStream.Close(); requestState.WebRequest.BeginGetResponse(ResponseCallback, requestState); } catch (Exception ex) { HandleResponseError(ex, requestState); } } private void ResponseCallback(IAsyncResult asyncResult) { var requestState = (RequestState)asyncResult.AsyncState; try { var webRequest = requestState.WebRequest; requestState.WebResponse = (HttpWebResponse)webRequest.EndGetResponse(asyncResult); ApplyWebResponseFilters(requestState.WebResponse); // Read the response into a Stream object. var responseStream = requestState.WebResponse.GetResponseStream(); requestState.ResponseStream = responseStream; responseStream.BeginRead(requestState.BufferRead, 0, BufferSize, ReadCallBack, requestState); return; } catch (Exception ex) { var firstCall = Interlocked.Increment(ref requestState.RequestCount) == 1; if (firstCall && WebRequestUtils.ShouldAuthenticate(ex, this.UserName, this.Password)) { try { requestState.WebRequest = (HttpWebRequest)WebRequest.Create(requestState.Url); requestState.WebRequest.AddBasicAuth(this.UserName, this.Password); if (OnAuthenticationRequired != null) { OnAuthenticationRequired(requestState.WebRequest); } SendWebRequestAsync( requestState.HttpMethod, requestState.Request, requestState, requestState.WebRequest); } catch (Exception /*subEx*/) { HandleResponseError(ex, requestState); } return; } HandleResponseError(ex, requestState); } } private void ReadCallBack(IAsyncResult asyncResult) { var requestState = (RequestState)asyncResult.AsyncState; try { var responseStream = requestState.ResponseStream; int read = responseStream.EndRead(asyncResult); if (read > 0) { requestState.BytesData.Write(requestState.BufferRead, 0, read); responseStream.BeginRead( requestState.BufferRead, 0, BufferSize, ReadCallBack, requestState); return; } Interlocked.Increment(ref requestState.Completed); var response = default(T); try { requestState.BytesData.Position = 0; using (var reader = requestState.BytesData) { response = (T)this.StreamDeserializer(typeof(T), reader); } #if SILVERLIGHT if (this.StoreCookies && this.ShareCookiesWithBrowser && !this.UseBrowserHttpHandling) { // browser cookies must be set on the ui thread System.Windows.Deployment.Current.Dispatcher.BeginInvoke( () => { var cookieHeader = this.CookieContainer.GetCookieHeader(new Uri(BaseUri)); System.Windows.Browser.HtmlPage.Document.Cookies = cookieHeader; }); } #endif requestState.HandleSuccess(response); } catch (Exception ex) { Log.Debug(string.Format("Error Reading Response Error: {0}", ex.Message), ex); requestState.HandleError(default(T), ex); } finally { responseStream.Close(); _webRequest = null; } } catch (Exception ex) { HandleResponseError(ex, requestState); } } private void HandleResponseError(Exception exception, RequestState requestState) { var webEx = exception as WebException; if (webEx != null #if !SILVERLIGHT && webEx.Status == WebExceptionStatus.ProtocolError #endif ) { var errorResponse = ((HttpWebResponse)webEx.Response); Log.Error(webEx); Log.DebugFormat("Status Code : {0}", errorResponse.StatusCode); Log.DebugFormat("Status Description : {0}", errorResponse.StatusDescription); var serviceEx = new WebServiceException(errorResponse.StatusDescription) { StatusCode = (int)errorResponse.StatusCode, }; try { using (var stream = errorResponse.GetResponseStream()) { //Uncomment to Debug exceptions: //var strResponse = new StreamReader(stream).ReadToEnd(); //Console.WriteLine("Response: " + strResponse); //stream.Position = 0; serviceEx.ResponseDto = this.StreamDeserializer(typeof(TResponse), stream); requestState.HandleError((TResponse)serviceEx.ResponseDto, serviceEx); } } catch (Exception innerEx) { // Oh, well, we tried Log.Debug(string.Format("WebException Reading Response Error: {0}", innerEx.Message), innerEx); requestState.HandleError(default(TResponse), new WebServiceException(errorResponse.StatusDescription, innerEx) { StatusCode = (int)errorResponse.StatusCode, }); } return; } var authEx = exception as AuthenticationException; if (authEx != null) { var customEx = WebRequestUtils.CreateCustomException(requestState.Url, authEx); Log.Debug(string.Format("AuthenticationException: {0}", customEx.Message), customEx); requestState.HandleError(default(TResponse), authEx); } Log.Debug(string.Format("Exception Reading Response Error: {0}", exception.Message), exception); requestState.HandleError(default(TResponse), exception); _webRequest = null; } private void ApplyWebResponseFilters(WebResponse webResponse) { if (!(webResponse is HttpWebResponse)) return; if (HttpWebResponseFilter != null) HttpWebResponseFilter((HttpWebResponse)webResponse); if (LocalHttpWebResponseFilter != null) LocalHttpWebResponseFilter((HttpWebResponse)webResponse); } private void ApplyWebRequestFilters(HttpWebRequest client) { if (LocalHttpWebRequestFilter != null) LocalHttpWebRequestFilter(client); if (HttpWebRequestFilter != null) HttpWebRequestFilter(client); } public void Dispose() { } } }