using System; using System.IO; using System.Net; #if !MONOTOUCH using System.Web; #endif using ServiceStack.Common; using ServiceStack.Logging; using ServiceStack.Service; using ServiceStack.ServiceHost; using ServiceStack.Text; namespace ServiceStack.ServiceClient.Web { /** * Need to provide async request options * http://msdn.microsoft.com/en-us/library/86wf6409(VS.71).aspx */ public abstract class ServiceClientBase #if !SILVERLIGHT : IServiceClient, IRestClient #else : IServiceClient #endif { private static readonly ILog log = LogManager.GetLogger(typeof(ServiceClientBase)); /// /// The request filter is called before any request. /// This request filter is executed globally. /// private static Action httpWebRequestFilter; public static Action HttpWebRequestFilter { get { return httpWebRequestFilter; } set { httpWebRequestFilter = value; AsyncServiceClient.HttpWebRequestFilter = value; } } /// /// 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 /// private static Action httpWebResponseFilter; public static Action HttpWebResponseFilter { get { return httpWebResponseFilter; } set { httpWebResponseFilter = value; AsyncServiceClient.HttpWebResponseFilter = value; } } public const string DefaultHttpMethod = "POST"; readonly AsyncServiceClient asyncClient; protected ServiceClientBase() { this.HttpMethod = DefaultHttpMethod; this.CookieContainer = new CookieContainer(); asyncClient = new AsyncServiceClient { ContentType = ContentType, StreamSerializer = SerializeToStream, StreamDeserializer = StreamDeserializer, CookieContainer = this.CookieContainer, UserName = this.UserName, Password = this.Password, LocalHttpWebRequestFilter = this.LocalHttpWebRequestFilter, LocalHttpWebResponseFilter = this.LocalHttpWebResponseFilter }; this.StoreCookies = true; //leave #if SILVERLIGHT asyncClient.HandleCallbackOnUIThread = this.HandleCallbackOnUIThread = true; asyncClient.UseBrowserHttpHandling = this.UseBrowserHttpHandling = false; asyncClient.ShareCookiesWithBrowser = this.ShareCookiesWithBrowser = true; #endif } protected ServiceClientBase(string syncReplyBaseUri, string asyncOneWayBaseUri) : this() { this.SyncReplyBaseUri = syncReplyBaseUri; this.AsyncOneWayBaseUri = asyncOneWayBaseUri; } /// /// Sets all baseUri properties, using the Format property for the SyncReplyBaseUri and AsyncOneWayBaseUri /// /// Base URI of the service public void SetBaseUri(string baseUri) { this.BaseUri = baseUri; this.asyncClient.BaseUri = baseUri; this.SyncReplyBaseUri = baseUri.WithTrailingSlash() + Format + "/syncreply/"; this.AsyncOneWayBaseUri = baseUri.WithTrailingSlash() + Format + "/asynconeway/"; } /// /// Sets all baseUri properties allowing for a temporary override of the Format property /// /// Base URI of the service /// Override of the Format property for the service //Marked obsolete on 4/11/2012 [Obsolete("Please call the SetBaseUri(string baseUri) method, which uses the specific implementation's Format property.")] public void SetBaseUri(string baseUri, string format) { this.BaseUri = baseUri; this.asyncClient.BaseUri = baseUri; this.SyncReplyBaseUri = baseUri.WithTrailingSlash() + format + "/syncreply/"; this.AsyncOneWayBaseUri = baseUri.WithTrailingSlash() + format + "/asynconeway/"; } private bool _disableAutoCompression; /// /// Whether to Accept Gzip,Deflate Content-Encoding and to auto decompress responses /// public bool DisableAutoCompression { get { return _disableAutoCompression; } set { _disableAutoCompression = value; asyncClient.DisableAutoCompression = value; } } private string _username; /// /// The user name for basic authentication /// public string UserName { get { return _username; } set { _username = value; asyncClient.UserName = value; } } private string _password; /// /// The password for basic authentication /// public string Password { get { return _password; } set { _password = value; asyncClient.Password = value; } } /// /// Sets the username and the password for basic authentication. /// public void SetCredentials(string userName, string password) { this.UserName = userName; this.Password = password; } public string BaseUri { get; set; } public abstract string Format { get; } public string SyncReplyBaseUri { get; set; } public string AsyncOneWayBaseUri { get; set; } private TimeSpan? timeout; public TimeSpan? Timeout { get { return this.timeout; } set { this.timeout = value; this.asyncClient.Timeout = value; } } public abstract string ContentType { get; } public string HttpMethod { get; set; } #if !SILVERLIGHT public IWebProxy Proxy { get; set; } #endif #if SILVERLIGHT private bool handleCallbackOnUiThread; public bool HandleCallbackOnUIThread { get { return this.handleCallbackOnUiThread; } set { asyncClient.HandleCallbackOnUIThread = this.handleCallbackOnUiThread = value; } } private bool useBrowserHttpHandling; public bool UseBrowserHttpHandling { get { return this.useBrowserHttpHandling; } set { asyncClient.UseBrowserHttpHandling = this.useBrowserHttpHandling = value; } } private bool shareCookiesWithBrowser; public bool ShareCookiesWithBrowser { get { return this.shareCookiesWithBrowser; } set { asyncClient.ShareCookiesWithBrowser = this.shareCookiesWithBrowser = value; } } #endif private ICredentials credentials; /// /// Gets or sets authentication information for the request. /// Warning: It's recommened to use and for basic auth. /// This property is only used for IIS level authentication. /// public ICredentials Credentials { set { this.credentials = value; this.asyncClient.Credentials = value; } } /// /// Determines if the basic auth header should be sent with every request. /// By default, the basic auth header is only sent when "401 Unauthorized" is returned. /// public bool AlwaysSendBasicAuthHeader { get; set; } /// /// Specifies if cookies should be stored /// private bool storeCookies; public bool StoreCookies { get { return storeCookies; } set { asyncClient.StoreCookies = storeCookies = value; } } public CookieContainer CookieContainer { get; set; } /// /// Called before request resend, when the initial request required authentication /// private Action onAuthenticationRequired { get; set; } public Action OnAuthenticationRequired { get { return onAuthenticationRequired; } set { onAuthenticationRequired = value; asyncClient.OnAuthenticationRequired = value; } } /// /// The request filter is called before any request. /// This request filter only works with the instance where it was set (not global). /// private Action localHttpWebRequestFilter { get; set; } public Action LocalHttpWebRequestFilter { get { return localHttpWebRequestFilter; } set { localHttpWebRequestFilter = value; asyncClient.LocalHttpWebRequestFilter = value; } } /// /// 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 /// private Action localHttpWebResponseFilter { get; set; } public Action LocalHttpWebResponseFilter { get { return localHttpWebResponseFilter; } set { localHttpWebResponseFilter = value; asyncClient.LocalHttpWebResponseFilter = value; } } public abstract void SerializeToStream(IRequestContext requestContext, object request, Stream stream); public abstract T DeserializeFromStream(Stream stream); public abstract StreamDeserializerDelegate StreamDeserializer { get; } #if !SILVERLIGHT public virtual TResponse Send(object request) { var requestUri = this.SyncReplyBaseUri.WithTrailingSlash() + request.GetType().Name; var client = SendRequest(requestUri, request); try { var webResponse = client.GetResponse(); ApplyWebResponseFilters(webResponse); using (var responseStream = webResponse.GetResponseStream()) { var response = DeserializeFromStream(responseStream); return response; } } catch (Exception ex) { TResponse response; if (!HandleResponseException(ex, Web.HttpMethod.Post, requestUri, request, out response)) { throw; } return response; } } private bool HandleResponseException(Exception ex, string httpMethod, string requestUri, object request, out TResponse response) { try { if (WebRequestUtils.ShouldAuthenticate(ex, this.UserName, this.Password)) { var client = SendRequest(httpMethod, requestUri, request); client.AddBasicAuth(this.UserName, this.Password); if (OnAuthenticationRequired != null) { OnAuthenticationRequired(client); } try { var webResponse = client.GetResponse(); ApplyWebResponseFilters(webResponse); using (var responseStream = webResponse.GetResponseStream()) { response = DeserializeFromStream(responseStream); return true; } } catch { /* Ignore deserializing error exceptions */ } } } catch (Exception subEx) { // Since we are effectively re-executing the call, // the new exception should be shown to the caller rather // than the old one. // The new exception is either this one or the one thrown // by the following method. HandleResponseException(subEx, requestUri); throw; } // If this doesn't throw, the calling method // should rethrow the original exception upon // return value of false. HandleResponseException(ex, requestUri); response = default(TResponse); return false; } private void HandleResponseException(Exception ex, string requestUri) { var webEx = ex as WebException; if (webEx != null && webEx.Status == WebExceptionStatus.ProtocolError) { 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, StatusDescription = errorResponse.StatusDescription, }; try { using (var stream = errorResponse.GetResponseStream()) { serviceEx.ResponseDto = DeserializeFromStream(stream); } } catch (Exception innerEx) { // Oh, well, we tried throw new WebServiceException(errorResponse.StatusDescription, innerEx) { StatusCode = (int)errorResponse.StatusCode, StatusDescription = errorResponse.StatusDescription, }; } //Escape deserialize exception handling and throw here throw serviceEx; } var authEx = ex as AuthenticationException; if (authEx != null) { throw WebRequestUtils.CreateCustomException(requestUri, authEx); } } private WebRequest SendRequest(string requestUri, object request) { return SendRequest(HttpMethod ?? DefaultHttpMethod, requestUri, request); } private WebRequest SendRequest(string httpMethod, string requestUri, object request) { if (httpMethod == null) throw new ArgumentNullException("httpMethod"); if (httpMethod == Web.HttpMethod.Get && request != null) { var queryString = QueryStringSerializer.SerializeToString(request); if (!string.IsNullOrEmpty(queryString)) { requestUri += "?" + queryString; } } var client = (HttpWebRequest)WebRequest.Create(requestUri); try { client.Accept = ContentType; client.Method = httpMethod; if (Proxy != null) client.Proxy = Proxy; if (this.Timeout.HasValue) client.Timeout = (int)this.Timeout.Value.TotalMilliseconds; if (this.credentials != null) client.Credentials = this.credentials; if (this.AlwaysSendBasicAuthHeader) client.AddBasicAuth(this.UserName, this.Password); if (!DisableAutoCompression) { client.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); client.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; } if (StoreCookies) { client.CookieContainer = CookieContainer; } ApplyWebRequestFilters(client); if (httpMethod != Web.HttpMethod.Get && httpMethod != Web.HttpMethod.Delete) { client.ContentType = ContentType; using (var requestStream = client.GetRequestStream()) { SerializeToStream(null, request, requestStream); } } } catch (AuthenticationException ex) { throw WebRequestUtils.CreateCustomException(requestUri, ex) ?? ex; } return client; } 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); } #else private void SendRequest(string requestUri, object request, Action callback) { var isHttpGet = HttpMethod != null && HttpMethod.ToUpper() == "GET"; if (isHttpGet) { var queryString = QueryStringSerializer.SerializeToString(request); if (!string.IsNullOrEmpty(queryString)) { requestUri += "?" + queryString; } } SendRequest(HttpMethod ?? DefaultHttpMethod, requestUri, request, callback); } private void SendRequest(string httpMethod, string requestUri, object request, Action callback) { if (httpMethod == null) throw new ArgumentNullException("httpMethod"); var client = (HttpWebRequest)WebRequest.Create(requestUri); try { client.Accept = ContentType; client.Method = httpMethod; if (this.credentials != null) client.Credentials = this.credentials; if (this.AlwaysSendBasicAuthHeader) client.AddBasicAuth(this.UserName, this.Password); if (StoreCookies) { client.CookieContainer = CookieContainer; } if (this.LocalHttpWebRequestFilter != null) LocalHttpWebRequestFilter(client); if (HttpWebRequestFilter != null) HttpWebRequestFilter(client); if (httpMethod != Web.HttpMethod.Get && httpMethod != Web.HttpMethod.Delete) { client.ContentType = ContentType; client.BeginGetRequestStream(delegate(IAsyncResult target) { var webReq = (HttpWebRequest)target.AsyncState; var requestStream = webReq.EndGetRequestStream(target); SerializeToStream(null, request, requestStream); callback(client); }, null); } } catch (AuthenticationException ex) { throw WebRequestUtils.CreateCustomException(requestUri, ex) ?? ex; } } #endif private string GetUrl(string relativeOrAbsoluteUrl) { return relativeOrAbsoluteUrl.StartsWith("http:") || relativeOrAbsoluteUrl.StartsWith("https:") ? relativeOrAbsoluteUrl : this.BaseUri.CombineWith(relativeOrAbsoluteUrl); } #if !SILVERLIGHT private byte[] DownloadBytes(string requestUri, object request) { var webRequest = SendRequest(requestUri, request); using (var response = webRequest.GetResponse()) { ApplyWebResponseFilters(response); using (var stream = response.GetResponseStream()) return stream.ReadFully(); } } #else private void DownloadBytes(string requestUri, object request, Action callback = null) { SendRequest(requestUri, request, webRequest => webRequest.BeginGetResponse(delegate(IAsyncResult result) { var webReq = (HttpWebRequest)result.AsyncState; var response = (HttpWebResponse)webReq.EndGetResponse(result); using (var stream = response.GetResponseStream()) { var bytes = stream.ReadFully(); if (callback != null) { callback(bytes); } } }, null)); } #endif public virtual void SendOneWay(object request) { var requestUri = this.AsyncOneWayBaseUri.WithTrailingSlash() + request.GetType().Name; DownloadBytes(requestUri, request); } public virtual void SendOneWay(string relativeOrAbsoluteUrl, object request) { var requestUri = GetUrl(relativeOrAbsoluteUrl); DownloadBytes(requestUri, request); } public virtual void SendAsync(object request, Action onSuccess, Action onError) { var requestUri = this.SyncReplyBaseUri.WithTrailingSlash() + request.GetType().Name; asyncClient.SendAsync(Web.HttpMethod.Post, requestUri, request, onSuccess, onError); } public virtual void GetAsync(string relativeOrAbsoluteUrl, Action onSuccess, Action onError) { asyncClient.SendAsync(Web.HttpMethod.Get, GetUrl(relativeOrAbsoluteUrl), null, onSuccess, onError); } public virtual void DeleteAsync(string relativeOrAbsoluteUrl, Action onSuccess, Action onError) { asyncClient.SendAsync(Web.HttpMethod.Delete, GetUrl(relativeOrAbsoluteUrl), null, onSuccess, onError); } public virtual void PostAsync(string relativeOrAbsoluteUrl, object request, Action onSuccess, Action onError) { asyncClient.SendAsync(Web.HttpMethod.Post, GetUrl(relativeOrAbsoluteUrl), request, onSuccess, onError); } public virtual void PutAsync(string relativeOrAbsoluteUrl, object request, Action onSuccess, Action onError) { asyncClient.SendAsync(Web.HttpMethod.Put, GetUrl(relativeOrAbsoluteUrl), request, onSuccess, onError); } public virtual void CancelAsync() { asyncClient.CancelAsync(); } #if !SILVERLIGHT public virtual TResponse Send(string httpMethod, string relativeOrAbsoluteUrl, object request) { var requestUri = GetUrl(relativeOrAbsoluteUrl); var client = SendRequest(httpMethod, requestUri, request); try { var webResponse = client.GetResponse(); ApplyWebResponseFilters(webResponse); using (var responseStream = webResponse.GetResponseStream()) { var response = DeserializeFromStream(responseStream); return response; } } catch (Exception ex) { TResponse response; if (!HandleResponseException(ex, httpMethod, requestUri, request, out response)) { throw; } return response; } } public virtual TResponse Get(string relativeOrAbsoluteUrl) { return Send(Web.HttpMethod.Get, relativeOrAbsoluteUrl, null); } public virtual TResponse Delete(string relativeOrAbsoluteUrl) { return Send(Web.HttpMethod.Delete, relativeOrAbsoluteUrl, null); } public virtual TResponse Post(string relativeOrAbsoluteUrl, object request) { return Send(Web.HttpMethod.Post, relativeOrAbsoluteUrl, request); } public virtual TResponse Put(string relativeOrAbsoluteUrl, object request) { return Send(Web.HttpMethod.Put, relativeOrAbsoluteUrl, request); } public virtual TResponse PostFileWithRequest(string relativeOrAbsoluteUrl, FileInfo fileToUpload, object request) { return PostFileWithRequest(relativeOrAbsoluteUrl, fileToUpload.OpenRead(), fileToUpload.Name, request); } public virtual TResponse PostFileWithRequest(string relativeOrAbsoluteUrl, Stream fileToUpload, string fileName, object request) { var requestUri = GetUrl(relativeOrAbsoluteUrl); var webRequest = (HttpWebRequest)WebRequest.Create(requestUri); webRequest.Method = Web.HttpMethod.Post; webRequest.Accept = ContentType; if (Proxy != null) webRequest.Proxy = Proxy; try { ApplyWebRequestFilters(webRequest); var queryString = QueryStringSerializer.SerializeToString(request); #if !MONOTOUCH var nameValueCollection = HttpUtility.ParseQueryString(queryString); #endif var boundary = DateTime.Now.Ticks.ToString(); webRequest.ContentType = "multipart/form-data; boundary=" + boundary; boundary = "--" + boundary; var newLine = Environment.NewLine; using (var outputStream = webRequest.GetRequestStream()) { #if !MONOTOUCH foreach (var key in nameValueCollection.AllKeys) { outputStream.Write(boundary + newLine); outputStream.Write("Content-Disposition: form-data;name=\"{0}\"{1}{2}".FormatWith(key, newLine, newLine)); outputStream.Write(nameValueCollection[key] + newLine); } #endif outputStream.Write(boundary + newLine); outputStream.Write("Content-Disposition: form-data;name=\"{0}\";filename=\"{1}\"{2}{3}".FormatWith("upload", fileName, newLine, newLine)); var buffer = new byte[4096]; int byteCount; while ((byteCount = fileToUpload.Read(buffer, 0, 4096)) > 0) { outputStream.Write(buffer, 0, byteCount); } outputStream.Write(newLine); outputStream.Write(boundary + "--"); } var webResponse = webRequest.GetResponse(); ApplyWebResponseFilters(webResponse); using (var responseStream = webResponse.GetResponseStream()) { var response = DeserializeFromStream(responseStream); return response; } } catch (Exception ex) { HandleResponseException(ex, requestUri); throw; } } public virtual TResponse PostFile(string relativeOrAbsoluteUrl, FileInfo fileToUpload, string mimeType) { var requestUri = GetUrl(relativeOrAbsoluteUrl); var webRequest = (HttpWebRequest)WebRequest.Create(requestUri); webRequest.Method = Web.HttpMethod.Post; webRequest.Accept = ContentType; if (Proxy != null) webRequest.Proxy = Proxy; try { ApplyWebRequestFilters(webRequest); var webResponse = webRequest.UploadFile(fileToUpload, mimeType); ApplyWebResponseFilters(webResponse); using (var responseStream = webResponse.GetResponseStream()) { var response = DeserializeFromStream(responseStream); return response; } } catch (Exception ex) { HandleResponseException(ex, requestUri); throw; } } public virtual TResponse PostFile(string relativeOrAbsoluteUrl, Stream fileToUpload, string fileName, string mimeType) { var requestUri = GetUrl(relativeOrAbsoluteUrl); var webRequest = (HttpWebRequest)WebRequest.Create(requestUri); webRequest.Method = Web.HttpMethod.Post; webRequest.Accept = ContentType; if (Proxy != null) webRequest.Proxy = Proxy; try { ApplyWebRequestFilters(webRequest); webRequest.UploadFile(fileToUpload, fileName, mimeType); var webResponse = webRequest.GetResponse(); ApplyWebResponseFilters(webResponse); using (var responseStream = webResponse.GetResponseStream()) { var response = DeserializeFromStream(responseStream); return response; } } catch (Exception ex) { HandleResponseException(ex, requestUri); throw; } } #endif public void Dispose() { } } }