Files
xIdentityService/Providers/XBaseIdentityHttpProvider.cs
T
2026-04-19 20:43:15 +03:30

973 lines
32 KiB
C#

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using RestSharp;
using RestSharp.Authenticators;
using xCommons.Constants;
using xCommons.Extensions;
using xCommons.Providers;
using xExceptions.Constants;
using xHttpService.Constants;
using xIdentityModels.Constants;
using xIdentityModels.Extensions;
using xIdentityModels.Models;
using xIdentityService.Constants;
using xIdentityService.Interfaces;
using xIdentityService.Models;
using xIdentityService.Extensions;
using System.Diagnostics.Tracing;
using MySqlX.XDevAPI.Common;
namespace xIdentityService.Providers
{
/// <summary>
/// an abstraction Implementation for Identity Provided Http Based Services ...
/// </summary>
public abstract class XBaseIdentityHttpProvider : IXBaseIdentityHttpProvider
{
/// <summary>
/// Client Base Url ...
/// </summary>
/// <value></value>
public string BaseUrl { get; }
/// <summary>
/// Logger ...
/// </summary>
/// <value></value>
public ILogger Logger { get; }
/// <summary>
/// Powered Value ...
/// </summary>
/// <value></value>
public string XPoweredValue { get; }
/// <summary>
/// Default Client Timeout ...
/// </summary>
/// <value></value>
public int DefaultClientTimeout { get; }
/// <summary>
/// Default Buffering Chunk Size ...
/// </summary>
/// <value></value>
public int DefaultBufferingChunckSize { get; }
/// <summary>
/// Validation Provider ...
/// </summary>
/// <value></value>
public XValidationProvider ValidationProvider { get; }
/// <summary>
/// Token Provider ...
/// </summary>
private readonly IXTokenProvider tokenProvider;
/// <summary>
/// Identity Provider ...
/// </summary>
private readonly IXIdentityProvider identityProvider;
public XBaseIdentityHttpProvider(
string baseUrl,
ILogger logger,
string poweredValue,
int defaultClientTimeout,
IXTokenProvider tokenProvider,
IXIdentityProvider identityProvider,
XValidationProvider validationProvider,
int defaultBufferingChunckSize = 8192
)
{
//
Logger = logger;
BaseUrl = baseUrl;
XPoweredValue = poweredValue;
this.tokenProvider = tokenProvider;
ValidationProvider = validationProvider;
this.identityProvider = identityProvider;
DefaultClientTimeout = defaultClientTimeout;
DefaultBufferingChunckSize = defaultBufferingChunckSize;
}
//
#region Tools ...
/// <summary>
/// Get an Instance of Http Client
/// </summary>
/// <returns></returns>
public HttpClient GetHttpClient(XTokenResponse tokens = null)
{
//
HttpClient httpClient = null;
//
// httpClient = new HttpClient ();
var httpClientHandler = new HttpClientHandler()
{
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) =>
{
//
Logger.LogInformation(
$"XSSL Handler: {Environment.NewLine}, sender: {sender}, {Environment.NewLine} cert: {cert}, {Environment.NewLine} chain: {chain}, {Environment.NewLine} sslPolicyErrors: {sslPolicyErrors}"
);
//
return true;
},
ClientCertificateOptions = ClientCertificateOption.Manual,
};
httpClient = new HttpClient(httpClientHandler);
//
// Set Authenticator ...
if (!tokens.IsNull() &&
!tokens.AccessToken.IsNullOrEmpty())
{
//
httpClient.DefaultRequestHeaders
.Add(
XAuthorization.Header,
$"{XAuthentication.BEARER} {tokens.AccessToken}");
}
//
// Refresh Token ...
if (!tokens.IsNull() &&
!tokens.RefreshToken.IsNullOrEmpty())
{
//
httpClient.DefaultRequestHeaders
.Add(
XAuthorization.RefreshToken,
tokens.RefreshToken);
}
//
// ExpiresAt ...
if (!tokens.IsNull() &&
tokens.ExpiresAt > 0)
{
//
httpClient.DefaultRequestHeaders
.Add(
XAuthorization.ExpiresAt,
tokens.ExpiresAt.AsHttpParamString());
}
//
// Set Default Timeout ...
httpClient.Timeout = TimeSpan.FromSeconds(DefaultClientTimeout == -1 ? 10000 : DefaultClientTimeout);
//
return httpClient;
}
public HttpClient GetHttpClient(XUserClaimsInfoDto userInfo = null)
{
//
XTokenResponse tokens = ToTokenResponse(userInfo);
//
return GetHttpClient(tokens);
}
/// <summary>
/// Get an Instance of RestClient
/// </summary>
/// <returns></returns>
public RestClient GetRestClient(XTokenResponse tokens = null)
{
//
var client = new RestClient(BaseUrl);
//
// Set Authenticator ...
if (!tokens.IsNull() &&
!tokens.AccessToken.IsNullOrEmpty())
{
client.Authenticator = new JwtAuthenticator(tokens.AccessToken);
}
//
// Refresh Token ...
if (!tokens.IsNull() &&
!tokens.RefreshToken.IsNullOrEmpty())
{
client.AddDefaultHeader(XAuthorization.RefreshToken, tokens.RefreshToken);
}
//
// ExpiresAt ...
if (!tokens.IsNull() &&
tokens.ExpiresAt > 0)
{
client.AddDefaultHeader(XAuthorization.ExpiresAt, tokens.ExpiresAt.AsHttpParamString());
}
//
// Set Default Timeout ...
client.Timeout = DefaultClientTimeout;
//
// SSL Validation Handler ...
client.RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) =>
{
//
Logger.LogInformation(
$"XSSL Handler: {Environment.NewLine}, sender: {sender}, {Environment.NewLine} cert: {cert}, {Environment.NewLine} chain: {chain}, {Environment.NewLine} sslPolicyErrors: {sslPolicyErrors}"
);
//
return true;
};
//
return client;
}
public RestClient GetRestClient(XUserClaimsInfoDto userInfo = null)
{
//
XTokenResponse tokens = ToTokenResponse(userInfo);
//
return GetRestClient(tokens);
}
/// <summary>
/// Get an Instance of RestRequest
/// </summary>
/// <param name="endpoint"></param>
/// <param name="addXPoweredValue"></param>
/// <param name=""></param>
/// <param name="headers"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
public RestRequest GetRestRequet(
Enum endpoint,
bool addXPoweredValue = true,
IDictionary<string, string> @params = null,
IDictionary<string, string> headers = null,
IDictionary<string, string> queryStrings = null
)
{
//
var url = PrepareUrl(
@params: @params,
endpoint: endpoint,
queryStrings: queryStrings
);
//
// Create request ...
var request = new RestRequest(url, DataFormat.Json);
//
if (addXPoweredValue)
{
request.AddHeader(XAuthorization.XPoweredBy, XPoweredValue);
}
//
// Add Headers to request ...
if (!headers.IsNull() && headers.Count > 0)
{
//
var enumerator = headers.GetEnumerator();
while (enumerator.MoveNext())
{
//
var item = enumerator.Current;
request.AddHeader(item.Key, item.Value);
}
}
//
return request;
}
/// <summary>
/// Handle Call Specific Request and retrieve Response
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="method"></param>
/// <param name="supportRefreshingTokens"></param>
/// <param name="cancellationToken"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public async Task<T> RunRestRequest<T>(
RestClient client,
RestRequest request,
XHttpMethod method,
bool supportRefreshingTokens = true,
CancellationToken cancellationToken = default
)
{
//
var response = await Task.Run(async () =>
{
//
IRestResponse resp = null;
//
// Call Endpoint Service using Specified Method ...
switch (method)
{
//
// Get ...
case XHttpMethod.GET:
resp = client.Get(request);
break;
//
// Put ...
case XHttpMethod.PUT:
resp = client.Put(request);
break;
//
// Post ...
case XHttpMethod.POST:
resp = client.Post(request);
break;
//
// Delete ...
case XHttpMethod.DELETE:
resp = client.Delete(request);
break;
}
//
if (!resp.IsSuccessful)
{
//
// Log Exception ...
Logger.LogError($"Error: {resp.ErrorMessage}");
Logger.LogError($"Exception: {resp.ErrorException.ToJSON()}");
if (!resp.ErrorMessage.IsNullOrEmpty() &&
resp.ErrorMessage.Contains("timed out"))
{
XException.Timeout.Throw();
}
//
// TODO: Check UnAuthorized Status here for
// Refresh Tokens or Issue Correct Exception ...
if (supportRefreshingTokens &&
resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
//
var isRefreshed = request.Parameters.Any(rp =>
rp.Name == XParam.XIsRefreshed.GetStringValue() &&
rp.Value.ToString().ToNormalString() == true.ToJSON());
//
Logger.LogInformation($"isRefreshed: {isRefreshed}");
//
// Get Access Token ...
XTokenResponse tokens = null;
var accessToken = request.Parameters.FirstOrDefault(rp =>
rp.Name.ToNormalString() == XAuthorization.Header.ToNormalString()).Value.ToString();
if (!accessToken.IsNullOrEmpty())
{
//
accessToken = accessToken.Replace(XAuthorization.TokenIdentifier, "");
//
if (!accessToken.IsNullOrEmpty())
{
tokens = await tokenProvider.RetrieveToken(accessToken);
}
}
//
// Refresh Token and then re do request ...
if (!isRefreshed &&
!accessToken.IsNullOrEmpty() &&
!tokens.IsNull() &&
tokens.IsRefreshable())
{
//
request.AddHeader(XParam.XIsRefreshed.GetStringValue(), true.ToJSON());
//
// Refreshe Tokens ...
var refreshedTokens = await identityProvider
.RefreshTokens(tokens);
//
// Renew Rest Client with Refreshed Tokens ...
var timeout = client.Timeout;
client = GetRestClient(refreshedTokens);
client.Timeout = Timeout.Infinite;
//
// Do Request Again using renew Client ...
return await RunRestRequest<T>(
client: client,
request: request,
method: method);
}
}
//
var ex = resp.Content.ToXError();
if (!ex.IsNull())
{
throw ex.ToException();
}
//
// UnAuthorized ...
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
XException.NotAuthorized.Throw();
}
//
// NotFound ...
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
{
XException.NotFound.Throw();
}
//
XException.ActionFailed.Throw();
}
//
var result = resp.Content.FromJSON<T>();
return result;
},
cancellationToken: cancellationToken);
//
return response;
}
/// <summary>
/// Access a Get Request Stream as AsyncEnumerable ...
/// </summary>
/// <param name="endpoint"></param>
/// <param name="bufferLength"></param>
/// <param name="addXPoweredValue"></param>
/// <param name="tokens"></param>
/// <param name="@params"></param>
/// <param name="headers"></param>
/// <param name="queryStrings"></param>
/// <param name="cancellationToken"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public async Task<Stream> StreamData(
Enum endpoint,
int bufferLength = 8194,
bool addXPoweredValue = true,
XTokenResponse tokens = null,
IDictionary<string, string> @params = null,
IDictionary<string, string> headers = null,
IDictionary<string, string> queryStrings = null,
[System.Runtime.CompilerServices.EnumeratorCancellation]
CancellationToken cancellationToken = default
)
{
//
var url = PrepareUrl(
@params: @params,
endpoint: endpoint,
attachBaseUrl: true,
queryStrings: queryStrings
);
var client = GetHttpClient(tokens);
if (addXPoweredValue)
{
//
client.DefaultRequestHeaders
.Add(
XAuthorization.XPoweredBy,
XPoweredValue
);
}
//
// For SSE, you might want to set the Accept header, though not strictly necessary for just streaming
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept
.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream"));
//
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
//
var stream = await response.Content.ReadAsStreamAsync();
//
return stream;
// using (var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
// {
// //
// int readedBuffers = 0;
// response.EnsureSuccessStatusCode();
// var buffer = new byte[bufferLength];
// var stream = await response.Content.ReadAsStreamAsync();
// //
// while ((readedBuffers = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
// {
// //
// var content = buffer.FromBytes();
// Console.WriteLine(content);
// }
// // await foreach (var message in stream) {
// // Console.WriteLine(message);
// // }
// // await foreach (var item in stream.ToAsyncEnumerable<byte[]>(cancellationToken: cancellationToken))
// // {
// // //
// // Console.WriteLine(item);
// // // T value = item.IsNull()
// // // ? default(T)
// // // : item.FromBytes().FromJSON<T>();
// // // //
// // // yield return value;
// // }
// }
///
///
///
///
//
// var eventSource = new EventSource(url);
// // eventSource.
// // var client = GetHttpClient(tokens);
// // if (addXPoweredValue)
// // {
// // //
// // client.DefaultRequestHeaders
// // .Add(
// // XAuthorization.XPoweredBy,
// // XPoweredValue);
// // }
// // //
// // // For SSE, you might want to set the Accept header, though not strictly necessary for just streaming
// // client.DefaultRequestHeaders.Accept.Clear();
// // client.DefaultRequestHeaders.Accept
// // .Add(
// // new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream"));
// // //
// // using (var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
// // {
// // //
// // response.EnsureSuccessStatusCode();
// // var stream = await response.Content.ReadAsStreamAsync();
// // await foreach (var item in stream.ToAsyncEnumerable<byte[]>(cancellationToken: cancellationToken))
// // {
// // //
// // T value = item.IsNull()
// // ? default(T)
// // : item.FromBytes().FromJSON<T>();
// // //
// // yield return value;
// // }
// // }
}
/// <summary>
/// Handle Call Specified Request and Retrieve XBaseRestResponse<T> ...
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="method"></param>
/// <param name="supportRefreshingTokens"></param>
/// <param name="cancellationToken"></param>
/// <typeparam name="T"></typeparam>
/// <returns>an instance of XBaseRestResponse<T></returns>
public async Task<XBaseRestResponse<T>> RunRestRequestByResponse<T>(
RestClient client,
RestRequest request,
XHttpMethod method,
bool supportRefreshingTokens = true,
CancellationToken cancellationToken = default
)
{
//
var response = await Task.Run(async () =>
{
//
IRestResponse resp = null;
//
// Call Endpoint Service using Specified Method ...
switch (method)
{
//
// Get ...
case XHttpMethod.GET:
resp = client.Get(request);
break;
//
// Put ...
case XHttpMethod.PUT:
resp = client.Put(request);
break;
//
// Post ...
case XHttpMethod.POST:
resp = client.Post(request);
break;
//
// Delete ...
case XHttpMethod.DELETE:
resp = client.Delete(request);
break;
}
//
if (!resp.IsSuccessful)
{
//
// Log Exception ...
Logger.LogError($"Error: {resp.ErrorMessage}");
Logger.LogError($"Exception: {resp.ErrorException.ToJSON()}");
if (!resp.ErrorMessage.IsNullOrEmpty() &&
resp.ErrorMessage.Contains("timed out"))
{
XException.Timeout.Throw();
}
//
// TODO: Check UnAuthorized Status here for
// Refresh Tokens or Issue Correct Exception ...
if (supportRefreshingTokens &&
resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
//
var isRefreshed = request.Parameters.Any(rp =>
rp.Name == XParam.XIsRefreshed.GetStringValue() &&
rp.Value.ToString().ToNormalString() == true.ToJSON());
//
Logger.LogInformation($"isRefreshed: {isRefreshed}");
//
// Get Access Token ...
XTokenResponse tokens = null;
var accessToken = request.Parameters.FirstOrDefault(rp =>
rp.Name.ToNormalString() == XAuthorization.Header.ToNormalString()).Value.ToString();
if (!accessToken.IsNullOrEmpty())
{
//
accessToken = accessToken.Replace(XAuthorization.TokenIdentifier, "");
//
if (!accessToken.IsNullOrEmpty())
{
tokens = await tokenProvider.RetrieveToken(accessToken);
}
}
//
// Refresh Token and then re do request ...
if (!isRefreshed &&
!accessToken.IsNullOrEmpty() &&
!tokens.IsNull() &&
tokens.IsRefreshable())
{
//
request.AddHeader(XParam.XIsRefreshed.GetStringValue(), true.ToJSON());
//
// Refreshe Tokens ...
var refreshedTokens = await identityProvider
.RefreshTokens(tokens);
//
// Renew Rest Client with Refreshed Tokens ...
var timeout = client.Timeout;
client = GetRestClient(refreshedTokens);
client.Timeout = Timeout.Infinite;
//
// Do Request Again using renew Client ...
return await RunRestRequestByResponse<T>(
client: client,
request: request,
method: method
);
}
}
//
var ex = resp.Content.ToXError();
if (!ex.IsNull())
{
throw ex.ToException();
}
//
// UnAuthorized ...
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
XException.NotAuthorized.Throw();
}
//
// NotFound ...
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
{
XException.NotFound.Throw();
}
//
XException.ActionFailed.Throw();
}
//
var result = new XBaseRestResponse<T>
{
Response = resp,
Model = resp.Content.FromJSON<T>()
};
//
return result;
},
cancellationToken: cancellationToken);
//
return response;
}
/// <summary>
/// Get User SelectBy Param
/// </summary>
/// <param name="model"></param>
/// <param name="forceNotNull"></param>
/// <param name="excludes"></param>
/// <returns></returns>
public string GetUserSelectByParam(
XActionRequest model,
bool forceNotNull = true,
ICollection<XUserSelectBy> excludes = null
)
{
//
// Validate Args ...
ValidationProvider.NotNull(model);
//
var id = model.UserId;
var userName = model.UserName;
var email = model.Email;
var phoneNumber = model.MobileNumber;
//
if (excludes != null)
{
//
if (excludes.Contains(XUserSelectBy.ID))
{
id = null;
}
if (excludes.Contains(XUserSelectBy.Email))
{
email = null;
}
if (excludes.Contains(XUserSelectBy.MobileNumber))
{
phoneNumber = null;
}
if (excludes.Contains(XUserSelectBy.Username))
{
userName = null;
}
}
var selectByParam =
id.IsNullOrEmpty() ?
userName.IsNullOrEmpty() ?
email.IsNullOrEmpty() ?
phoneNumber.IsNullOrEmpty() ? null : phoneNumber : email : userName : id;
//
// Check result ...
if (forceNotNull &&
selectByParam.IsNullOrEmpty())
{
XException.InvalidData.Throw();
}
//
return selectByParam;
}
/// <summary>
/// Prepare Url Address of Specified Enum by Filling Params
/// and Query Strings Attach ...
/// </summary>
/// <param name="endpoint"></param>
/// <param name="attachBaseUrl"></param>
/// <param name="@params"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
public string PrepareUrl(
Enum endpoint,
bool attachBaseUrl = false,
IDictionary<string, string> @params = null,
IDictionary<string, string> queryStrings = null
)
{
//
// Retrieving Url Template ...
var url = endpoint.GetStringValue();
//
// Filling Parmas and Query Strings of Url ...
url = PrepareUrl(
url: url,
@params: @params,
queryStrings: queryStrings,
attachBaseUrl: attachBaseUrl
);
//
return url;
}
/// <summary>
/// Prepare Url Address by Filling Params
/// and Query Strings Attach ...
/// </summary>
/// <param name="url"></param>
/// <param name="attachBaseUrl"></param>
/// <param name="@params"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
public string PrepareUrl(
string url,
bool attachBaseUrl = false,
IDictionary<string, string> @params = null,
IDictionary<string, string> queryStrings = null
)
{
//
if (attachBaseUrl)
{
//
var baseUrl = BaseUrl;
if (!baseUrl.EndsWith("/"))
{
baseUrl = $"{baseUrl}/";
}
//
url = $"{baseUrl}{url}";
}
//
// Add Route Payloads ...
if (!@params.IsNull() && @params.Count > 0)
{
//
var enumerator = @params.GetEnumerator();
while (enumerator.MoveNext())
{
//
var item = enumerator.Current;
//
url = url.Replace(item.Key, item.Value.ToUrlEncoded());
}
}
//
// Add Query Strings ...
if (!queryStrings.IsNull() && queryStrings.Count > 0)
{
//
url = $"{url}?";
var enumerator = queryStrings.GetEnumerator();
while (enumerator.MoveNext())
{
//
var item = enumerator.Current;
//
url += $"{item.Key}={item.Value.ToUrlEncoded()}&";
}
//
// remove lates & ...
if (url.EndsWith("&"))
{
url = url.Substring(0, url.Length - 1);
}
}
//
return url;
}
/// <summary>
/// Converts UserInfo to TokenResponse model ...
/// </summary>
/// <param name="userInfo"></param>
/// <returns></returns>
public XTokenResponse ToTokenResponse(XUserClaimsInfoDto userInfo = null)
{
//
XTokenResponse result = null;
//
if (!userInfo.IsNull())
{
//
result = new XTokenResponse
{
AccessToken = userInfo.AccessToken.IsNullOrEmpty()
? ""
: userInfo.AccessToken,
RefreshToken = userInfo.RefreshToken.IsNullOrEmpty()
? ""
: userInfo.RefreshToken,
ExpiresAt = userInfo.ExpiresAt,
};
//
if (userInfo.AccessToken.IsNullOrEmpty())
{
result = null;
}
}
//
return result;
}
#endregion
}
}