// Copyright (c) ServiceStack, Inc. All Rights Reserved.
// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt
using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using ServiceStack.Logging;
using ServiceStack.Text;
using ServiceStack.Web;
namespace ServiceStack
{
/**
* Need to provide async request options
* http://msdn.microsoft.com/en-us/library/86wf6409(VS.71).aspx
*/
public partial class AsyncServiceClient : IHasSessionId, IHasBearerToken, IHasVersion
{
private static readonly ILog Log = LogManager.GetLogger(typeof(AsyncServiceClient));
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60);
//private HttpWebRequest webRequest = null;
private AuthenticationInfo authInfo = null;
///
/// The request filter is called before any request.
/// This request filter is executed globally.
///
public static Action GlobalRequestFilter { 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 GlobalResponseFilter { get; set; }
///
/// Called before request resend, when the initial request required authentication
///
public Action OnAuthenticationRequired { get; set; }
public string RefreshToken { get; set; }
public string RefreshTokenUri { get; set; }
public bool UseTokenCookie { get; set; }
public static int BufferSize = 8192;
public ICredentials Credentials { get; set; }
public bool AlwaysSendBasicAuthHeader { get; set; }
public bool StoreCookies { get; set; }
public NameValueCollection Headers { 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 RequestFilter { 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 ResponseFilter { get; set; }
///
/// The ResultsFilter is called before the Request is sent allowing you to return a cached response.
///
public ResultsFilterDelegate ResultsFilter { get; set; }
///
/// The ResultsFilterResponse is called before returning the response allowing responses to be cached.
///
public ResultsFilterResponseDelegate ResultsFilterResponse { get; set; }
///
/// Called with requestUri, ResponseType when server returns 304 NotModified
///
public ExceptionFilterDelegate ExceptionFilter { get; set; }
public string BaseUri { get; set; }
public bool DisableAutoCompression { get; set; }
public string RequestCompressionType { 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; }
public string UserAgent { get; set; }
public bool EmulateHttpViaPost { get; set; }
public ProgressDelegate OnDownloadProgress { get; set; }
public ProgressDelegate OnUploadProgress { get; set; }
public bool ShareCookiesWithBrowser { get; set; }
public IWebProxy Proxy { get; set; }
public int Version { get; set; }
public string SessionId { get; set; }
public string BearerToken { get; set; }
public static bool DisableTimer { get; set; }
public Task SendAsync(string httpMethod, string absoluteUrl, object request, CancellationToken token = default(CancellationToken))
{
if (ResultsFilter != null)
{
var response = ResultsFilter(typeof(TResponse), httpMethod, absoluteUrl, request);
if (response is TResponse typedResponse)
return typedResponse.InTask();
}
return SendWebRequestAsync(httpMethod, absoluteUrl, request, token);
}
private async Task SendWebRequestAsync(string httpMethod, string absoluteUrl, object request, CancellationToken token, bool recall = false)
{
if (httpMethod == null)
throw new ArgumentNullException(nameof(httpMethod));
this.PopulateRequestMetadata(request);
var requestUri = absoluteUrl;
var hasQueryString = request != null && !HttpUtils.HasRequestBody(httpMethod);
if (hasQueryString)
{
var queryString = QueryStringSerializer.SerializeToString(request);
if (!string.IsNullOrEmpty(queryString))
{
requestUri += "?" + queryString;
}
}
var webReq = this.CreateHttpWebRequest(requestUri);
if (webReq != null) webReq.Proxy = Proxy;
var timedOut = false;
ITimer timer = null;
timer = PclExportClient.Instance.CreateTimer(state =>
{
timedOut = true;
webReq?.Abort();
webReq = null;
timer?.Cancel();
timer = null;
}, this.Timeout.GetValueOrDefault(DefaultTimeout), this);
Exception ResolveException(Exception ex)
{
if (token.IsCancellationRequested)
return new OperationCanceledException(token);
if (timedOut)
return PclExportClient.Instance.CreateTimeoutException(ex, "The request timed out");
return ex;
}
bool returningWebResponse = false;
HttpWebResponse webRes = null;
T Complete(T response)
{
timer.Cancel();
PclExportClient.Instance.SynchronizeCookies(this);
ResultsFilterResponse?.Invoke(webRes, response, httpMethod, absoluteUrl, request);
return response;
}
webReq.Accept = ContentType;
if (this.EmulateHttpViaPost)
{
webReq.Method = "POST";
webReq.Headers[HttpHeaders.XHttpMethodOverride] = httpMethod;
}
else
{
webReq.Method = httpMethod;
}
PclExportClient.Instance.AddHeader(webReq, Headers);
PclExport.Instance.Config(webReq, userAgent: UserAgent);
if (this.authInfo != null && !string.IsNullOrEmpty(this.UserName))
webReq.AddAuthInfo(this.UserName, this.Password, authInfo);
else if (this.BearerToken != null)
webReq.Headers[HttpHeaders.Authorization] = "Bearer " + this.BearerToken;
else if (this.Credentials != null)
webReq.Credentials = this.Credentials;
else if (this.AlwaysSendBasicAuthHeader)
webReq.AddBasicAuth(this.UserName, this.Password);
if (!DisableAutoCompression)
{
PclExport.Instance.AddCompression(webReq);
}
ApplyWebRequestFilters(webReq);
try
{
if (HttpUtils.HasRequestBody(webReq.Method))
{
webReq.ContentType = ContentType;
if (RequestCompressionType != null)
webReq.Headers[HttpHeaders.ContentEncoding] = RequestCompressionType;
using (var requestStream = await webReq.GetRequestStreamAsync().ConfigureAwait(false))
{
token.ThrowIfCancellationRequested();
if (request != null)
{
StreamSerializer(null, request, requestStream);
}
}
}
}
catch (Exception ex)
{
if (Log.IsDebugEnabled)
Log.Debug($"Error Sending Request: {ex.Message}", ex);
throw HandleResponseError(ResolveException(ex), requestUri, request);
}
try
{
webRes = (HttpWebResponse) await webReq.GetResponseAsync().ConfigureAwait(false);
{
token.ThrowIfCancellationRequested();
ApplyWebResponseFilters(webRes);
returningWebResponse = typeof(T) == typeof(HttpWebResponse);
if (returningWebResponse)
return Complete((T) (object) webRes);
var responseStream = webRes.ResponseStream();
var responseBodyLength = webRes.ContentLength;
var bufferRead = new byte[BufferSize];
var totalRead = 0;
int read;
var ms = MemoryStreamFactory.GetStream();
while ((read = await responseStream.ReadAsync(bufferRead, 0, bufferRead.Length, token).ConfigureAwait(false)) != 0)
{
ms.Write(bufferRead, 0, read);
totalRead += read;
OnDownloadProgress?.Invoke(totalRead, responseBodyLength);
}
try
{
ms.Position = 0;
if (typeof(T) == typeof(Stream))
{
return Complete((T) (object) ms);
}
else
{
var stream = ms;
try
{
if (typeof(T) == typeof(string))
{
return Complete((T) (object) stream.ReadToEnd());
}
else if (typeof(T) == typeof(byte[]))
return Complete((T) (object) stream.ToArray());
else
return Complete((T) this.StreamDeserializer(typeof(T), stream));
}
finally
{
if (stream.CanRead)
stream.Dispose(); // Not yet disposed, but could've been.
}
}
}
catch (Exception ex)
{
if (Log.IsDebugEnabled)
Log.Debug($"Error Reading Response Error: {ex.Message}", ex);
throw;
}
finally
{
responseStream.Close();
}
}
}
catch (Exception ex)
{
var webEx = ex as WebException;
var firstCall = !recall;
if (firstCall && WebRequestUtils.ShouldAuthenticate(webEx,
(!string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password))
|| Credentials != null
|| BearerToken != null
|| RefreshToken != null
|| OnAuthenticationRequired != null))
{
try
{
if (RefreshToken != null)
{
var refreshRequest = new GetAccessToken {
RefreshToken = RefreshToken,
UseTokenCookie = UseTokenCookie,
};
var uri = this.RefreshTokenUri ?? this.BaseUri.CombineWith(refreshRequest.ToPostUrl());
GetAccessTokenResponse tokenResponse;
try
{
tokenResponse = uri.PostJsonToUrl(refreshRequest, requestFilter: req => {
if (UseTokenCookie) {
req.CookieContainer = CookieContainer;
}
}).FromJson();
}
catch (WebException refreshEx)
{
var webServiceEx = ServiceClientBase.ToWebServiceException(refreshEx,
stream => StreamDeserializer(typeof(T), stream),
ContentType);
if (webServiceEx != null)
throw new RefreshTokenException(webServiceEx);
throw new RefreshTokenException(refreshEx.Message, refreshEx);
}
var accessToken = tokenResponse?.AccessToken;
var refreshClient = webReq = (HttpWebRequest) WebRequest.Create(requestUri);
var tokenCookie = this.CookieContainer.GetTokenCookie(BaseUri);
if (UseTokenCookie)
{
if (tokenCookie == null)
throw new RefreshTokenException("Could not retrieve new AccessToken Cooke from: " + uri);
refreshClient.CookieContainer.SetTokenCookie(BaseUri, tokenCookie);
}
else
{
if (string.IsNullOrEmpty(accessToken))
throw new RefreshTokenException("Could not retrieve new AccessToken from: " + uri);
if (tokenCookie != null)
{
this.CookieContainer.SetTokenCookie(accessToken, BaseUri);
refreshClient.CookieContainer.SetTokenCookie(BaseUri, accessToken);
}
else
{
refreshClient.AddBearerToken(this.BearerToken = accessToken);
}
}
return await SendWebRequestAsync(httpMethod, absoluteUrl, request, token, recall: true).ConfigureAwait(false);
}
OnAuthenticationRequired?.Invoke();
var newReq = (HttpWebRequest) WebRequest.Create(requestUri);
if (StoreCookies)
newReq.CookieContainer = CookieContainer;
HandleAuthException(ex, webReq);
return await SendWebRequestAsync(httpMethod, absoluteUrl, request, token, recall: true).ConfigureAwait(false);
}
catch (WebServiceException)
{
throw;
}
catch (Exception /*subEx*/)
{
throw HandleResponseError(ResolveException(ex), requestUri, request);
}
}
if (ExceptionFilter != null && webEx?.Response != null)
{
var cachedResponse = ExceptionFilter(webEx, webEx.Response, requestUri, typeof(T));
if (cachedResponse is T variable)
return variable;
}
throw HandleResponseError(ResolveException(ex), requestUri, request);
}
finally
{
if (!returningWebResponse)
webRes?.Dispose();
}
}
private Exception HandleResponseError(Exception exception, string url, object request)
{
var webEx = exception as WebException;
if (PclExportClient.Instance.IsWebException(webEx))
{
var errorResponse = (HttpWebResponse)webEx.Response;
Log.Error(webEx);
if (Log.IsDebugEnabled)
{
Log.Debug($"Status Code : {errorResponse.StatusCode}");
Log.Debug($"Status Description : {errorResponse.StatusDescription}");
}
var serviceEx = new WebServiceException(errorResponse.StatusDescription)
{
StatusCode = (int)errorResponse.StatusCode,
StatusDescription = errorResponse.StatusDescription,
ResponseHeaders = errorResponse.Headers
};
try
{
using (var stream = errorResponse.ResponseStream())
{
var bytes = stream.ReadFully();
serviceEx.ResponseBody = bytes.FromUtf8Bytes();
var errorResponseType = WebRequestUtils.GetErrorResponseDtoType(request);
if (stream.CanSeek)
{
PclExport.Instance.ResetStream(stream);
serviceEx.ResponseDto = this.StreamDeserializer(errorResponseType, stream);
}
else //Android
{
using (var ms = MemoryStreamFactory.GetStream(bytes))
{
serviceEx.ResponseDto = this.StreamDeserializer(errorResponseType, ms);
}
}
return serviceEx;
}
}
catch (Exception innerEx)
{
// Oh, well, we tried
Log.Debug($"WebException Reading Response Error: {innerEx.Message}", innerEx);
return new WebServiceException(errorResponse.StatusDescription, innerEx)
{
StatusCode = (int)errorResponse.StatusCode,
StatusDescription = errorResponse.StatusDescription,
ResponseHeaders = errorResponse.Headers
};
}
}
if (exception is AuthenticationException authEx)
{
var customEx = WebRequestUtils.CreateCustomException(url, authEx);
Log.Debug($"AuthenticationException: {customEx.Message}", customEx);
return authEx;
}
Log.Debug($"Exception Reading Response Error: {exception.Message}", exception);
return exception;
}
private void HandleAuthException(Exception ex, WebRequest client)
{
if (ex is WebException webEx && webEx.Response != null)
{
var headers = ((HttpWebResponse)webEx.Response).Headers;
var doAuthHeader = PclExportClient.Instance.GetHeader(headers,
HttpHeaders.WwwAuthenticate, x => x.Contains("realm"));
if (doAuthHeader == null)
{
client.AddBasicAuth(this.UserName, this.Password);
}
else
{
this.authInfo = new AuthenticationInfo(doAuthHeader);
client.AddAuthInfo(this.UserName, this.Password, authInfo);
}
}
}
private void ApplyWebResponseFilters(WebResponse webResponse)
{
if (!(webResponse is HttpWebResponse)) return;
ResponseFilter?.Invoke((HttpWebResponse)webResponse);
GlobalResponseFilter?.Invoke((HttpWebResponse)webResponse);
}
private void ApplyWebRequestFilters(HttpWebRequest client)
{
RequestFilter?.Invoke(client);
GlobalRequestFilter?.Invoke(client);
}
public void Dispose() { }
}
}