Refactor client classes

This commit is contained in:
2025-08-24 18:20:57 +09:00
parent 1f025dd62e
commit a76d283936
55 changed files with 10900 additions and 0 deletions

View File

@ -0,0 +1,9 @@
namespace Hcs.Client.Api.Request
{
public interface IAck
{
string MessageGUID { get; set; }
string RequesterMessageGUID { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Hcs.Client.Api.Request.Adapter
{
public interface IAsyncClient<TRequestHeader> where TRequestHeader : class
{
Task<IGetStateResponse> GetStateAsync(TRequestHeader header, IGetStateRequest request);
}
}

View File

@ -0,0 +1,9 @@
namespace Hcs.Client.Api.Request
{
public interface IErrorMessage
{
string ErrorCode { get; }
string Description { get; }
}
}

View File

@ -0,0 +1,7 @@
namespace Hcs.Client.Api.Request
{
public interface IGetStateRequest
{
string MessageGUID { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace Hcs.Client.Api.Request.Adapter
{
public interface IGetStateResponse
{
IGetStateResult GetStateResult { get; }
}
}

View File

@ -0,0 +1,7 @@
namespace Hcs.Client.Api.Request.Adapter
{
public interface IGetStateResult
{
sbyte RequestState { get; }
}
}

View File

@ -0,0 +1,7 @@
namespace Hcs.Client.Api.Request.Adapter
{
public interface IGetStateResultMany : IGetStateResult
{
object[] Items { get; }
}
}

View File

@ -0,0 +1,7 @@
namespace Hcs.Client.Api.Request.Adapter
{
public interface IGetStateResultOne : IGetStateResult
{
object Item { get; }
}
}

View File

@ -0,0 +1,9 @@
namespace Hcs.Client.Api.Request
{
internal enum AsyncRequestStateType
{
Received = 1,
InProgress,
Ready
}
}

View File

@ -0,0 +1,16 @@
namespace Hcs.Client.Api.Request
{
internal enum EndPoint
{
BillsAsync,
DebtRequestsAsync,
DeviceMeteringAsync,
HomeManagementAsync,
LicensesAsync,
NsiAsync,
NsiCommonAsync,
OrgRegistryAsync,
OrgRegistryCommonAsync,
PaymentsAsync
}
}

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace Hcs.Client.Api.Request
{
internal class EndPointLocator
{
private static readonly Dictionary<EndPoint, string> endPoints;
static EndPointLocator()
{
endPoints ??= [];
endPoints.Add(EndPoint.BillsAsync, "ext-bus-bills-service/services/BillsAsync");
endPoints.Add(EndPoint.DebtRequestsAsync, "ext-bus-debtreq-service/services/DebtRequestsAsync");
endPoints.Add(EndPoint.DeviceMeteringAsync, "ext-bus-device-metering-service/services/DeviceMeteringAsync");
endPoints.Add(EndPoint.HomeManagementAsync, "ext-bus-home-management-service/services/HomeManagementAsync");
endPoints.Add(EndPoint.LicensesAsync, "ext-bus-licenses-service/services/LicensesAsync");
endPoints.Add(EndPoint.NsiAsync, "ext-bus-nsi-service/services/NsiAsync");
endPoints.Add(EndPoint.NsiCommonAsync, "ext-bus-nsi-common-service/services/NsiCommonAsync");
endPoints.Add(EndPoint.OrgRegistryAsync, "ext-bus-org-registry-service/services/OrgRegistryAsync");
endPoints.Add(EndPoint.OrgRegistryCommonAsync, "ext-bus-org-registry-common-service/services/OrgRegistryCommonAsync");
endPoints.Add(EndPoint.PaymentsAsync, "ext-bus-payment-service/services/PaymentAsync");
}
internal static string GetPath(EndPoint endPoint)
{
return endPoints[endPoint];
}
}
}

View File

@ -0,0 +1,17 @@
namespace Hcs.Client.Api.Request.Exception
{
/// <summary>
/// Исключение указывает на то, что сервер обнаружил что у
/// него нет объектов для выдачи по условию
/// </summary>
internal class NoResultsRemoteException : RemoteException
{
public NoResultsRemoteException(string description) : base(NO_OBJECTS_FOR_EXPORT, description) { }
public NoResultsRemoteException(string errorCode, string description) :
base(errorCode, description) { }
public NoResultsRemoteException(string errorCode, string description, System.Exception nested) :
base(errorCode, description, nested) { }
}
}

View File

@ -0,0 +1,65 @@
using Hcs.Client.Internal;
using System;
using System.Linq;
namespace Hcs.Client.Api.Request.Exception
{
internal class RemoteException : System.Exception
{
internal const string NO_OBJECTS_FOR_EXPORT = "INT002012";
internal const string MISSING_IN_REGISTRY = "INT002000";
internal const string ACCESS_DENIED = "AUT011003";
internal string ErrorCode { get; private set; }
internal string Description { get; private set; }
public RemoteException(string errorCode, string description)
: base(Combine(errorCode, description))
{
ErrorCode = errorCode;
Description = description;
}
public RemoteException(string errorCode, string description, System.Exception nestedException)
: base(Combine(errorCode, description), nestedException)
{
ErrorCode = errorCode;
Description = description;
}
private static string Combine(string errorCode, string description)
{
return $"Remote server returned an error: [{errorCode}] {description}";
}
internal static RemoteException CreateNew(string errorCode, string description, System.Exception nested = null)
{
if (string.Compare(errorCode, NO_OBJECTS_FOR_EXPORT) == 0)
{
return new NoResultsRemoteException(errorCode, description, nested);
}
return new RemoteException(errorCode, description);
}
internal static RemoteException CreateNew(RemoteException nested)
{
if (nested == null)
{
throw new ArgumentNullException(nameof(nested));
}
return CreateNew(nested.ErrorCode, nested.Description, nested);
}
/// <summary>
/// Возвращает true, если ошибка @e или ее вложенные ошибки содержат @errorCode
/// </summary>
internal static bool ContainsErrorCode(SystemException e, string errorCode)
{
if (e == null)
{
return false;
}
return Util.EnumerateInnerExceptions(e).OfType<RemoteException>().Where(x => x.ErrorCode == errorCode).Any();
}
}
}

View File

@ -0,0 +1,9 @@
namespace Hcs.Client.Api.Request.Exception
{
internal class RestartTimeoutException : System.Exception
{
public RestartTimeoutException(string message) : base(message) { }
public RestartTimeoutException(string message, System.Exception innerException) : base(message, innerException) { }
}
}

View File

@ -0,0 +1,23 @@
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
namespace Hcs.Client.Api.Request
{
internal class GostSigningEndpointBehavior(ClientBase client) : IEndpointBehavior
{
private readonly ClientBase client = client;
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(
new GostSigningMessageInspector(client));
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }
public void Validate(ServiceEndpoint endpoint) { }
}
}

View File

@ -0,0 +1,116 @@
using Hcs.Client.Internal;
using Hcs.GostXades;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Text;
using System.Xml;
namespace Hcs.Client.Api.Request
{
/// <summary>
/// Фильтр сообщений добавляет в XML-сообщение электронную подпись XADES/GOST
/// </summary>
internal class GostSigningMessageInspector(ClientBase client) : IClientMessageInspector
{
private readonly ClientBase client = client;
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
try
{
var filterHeader = "[Message Inspector] ";
PurgeDebuggerHeaders(ref request);
var messageBody = GetMessageBodyString(ref request, Encoding.UTF8);
if (!messageBody.Contains(Constants.SIGNED_XML_ELEMENT_ID))
{
client.TryCaptureMessage(true, messageBody);
}
else
{
var certInfo = X509Tools.GetFullnameWithExpirationDateStr(client.Certificate);
client.TryLog($"{filterHeader} signing message with key [{certInfo}]...");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var service = new GostXadesBesService(client.CryptoProviderType);
var signedXml = service.Sign(messageBody,
Constants.SIGNED_XML_ELEMENT_ID,
client.CertificateThumbprint,
client.CertificatePassword);
stopwatch.Stop();
client.TryLog($"{filterHeader} message signed in {stopwatch.ElapsedMilliseconds} ms");
client.TryCaptureMessage(true, signedXml);
request = Message.CreateMessage(
XmlReaderFromString(signedXml), int.MaxValue, request.Version);
}
}
catch (System.Exception ex)
{
throw new System.Exception($"Exception occured in {GetType().Name}", ex);
}
return null;
}
private void PurgeDebuggerHeaders(ref Message request)
{
var limit = request.Headers.Count;
for (var i = 0; i < limit; ++i)
{
if (request.Headers[i].Name.Equals("VsDebuggerCausalityData"))
{
request.Headers.RemoveAt(i);
break;
}
}
}
private string GetMessageBodyString(ref Message request, Encoding encoding)
{
var mb = request.CreateBufferedCopy(int.MaxValue);
request = mb.CreateMessage();
var s = new MemoryStream();
var xw = XmlWriter.Create(s);
mb.CreateMessage().WriteMessage(xw);
xw.Flush();
s.Position = 0;
var bXML = new byte[s.Length];
s.Read(bXML, 0, (int)s.Length);
if (bXML[0] != (byte)'<')
{
return encoding.GetString(bXML, 3, bXML.Length - 3);
}
else
{
return encoding.GetString(bXML, 0, bXML.Length);
}
}
private XmlReader XmlReaderFromString(string xml)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(xml);
writer.Flush();
stream.Position = 0;
return XmlReader.Create(stream);
}
public void AfterReceiveReply(ref Message reply, object correlationState)
{
client.TryCaptureMessage(false, reply.ToString());
}
}
}

View File

@ -0,0 +1,31 @@
using Hcs.Client.Internal;
using Hcs.Service.Async.Nsi;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Hcs.Client.Api.Request.Nsi
{
internal class ExportDataProviderNsiItemRequest(ClientBase client) : NsiRequestBase(client)
{
internal async Task<IEnumerable<NsiItemType>> ExecuteAsync(exportDataProviderNsiItemRequestRegistryNumber registryNumber, CancellationToken token)
{
// http://open-gkh.ru/Nsi/exportDataProviderNsiItemRequest.html
var request = new exportDataProviderNsiItemRequest
{
Id = Constants.SIGNED_XML_ELEMENT_ID,
version = "10.0.1.2",
RegistryNumber = registryNumber,
};
var stateResult = await SendAndWaitResultAsync(request, async(portClient) =>
{
var response = await portClient.exportDataProviderNsiItemAsync(CreateRequestHeader(), request);
return response.AckRequest.Ack;
}, token);
return stateResult.Items.OfType<NsiItemType>();
}
}
}

View File

@ -0,0 +1,49 @@
using Hcs.Client.Api.Request;
using Hcs.Client.Api.Request.Adapter;
using Hcs.Service.Async.Nsi;
using System.Threading.Tasks;
namespace Hcs.Service.Async.Nsi
{
public partial class getStateResult : IGetStateResultMany { }
public partial class NsiPortsTypeAsyncClient : IAsyncClient<RequestHeader>
{
public async Task<IGetStateResponse> GetStateAsync(RequestHeader header, IGetStateRequest request)
{
return await getStateAsync(header, (getStateRequest)request);
}
}
public partial class getStateResponse : IGetStateResponse
{
public IGetStateResult GetStateResult => getStateResult;
}
public partial class AckRequestAck : IAck { }
public partial class ErrorMessageType : IErrorMessage { }
public partial class getStateRequest : IGetStateRequest { }
}
namespace Hcs.Client.Api.Request.Nsi
{
internal class NsiRequestBase(ClientBase client) :
RequestBase<getStateResult,
NsiPortsTypeAsyncClient,
NsiPortsTypeAsync,
RequestHeader,
AckRequestAck,
ErrorMessageType,
getStateRequest>(client)
{
protected override EndPoint EndPoint => EndPoint.NsiAsync;
protected override bool EnableMinimalResponseWaitDelay => true;
protected override bool CanBeRestarted => true;
protected override int RestartTimeoutMinutes => 20;
}
}

View File

@ -0,0 +1,407 @@
using Hcs.Client.Api.Request.Adapter;
using Hcs.Client.Api.Request.Exception;
using Hcs.Client.Internal;
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Hcs.Client.Api.Request
{
internal abstract class RequestBase<TResult, TAsyncClient, TChannel, TRequestHeader, TAck, TErrorMessage, TGetStateRequest>
where TResult : IGetStateResult
where TAsyncClient : ClientBase<TChannel>, TChannel, IAsyncClient<TRequestHeader>
where TChannel : class
where TRequestHeader : class
where TAck : IAck
where TErrorMessage : IErrorMessage
where TGetStateRequest : IGetStateRequest, new()
{
private const int RESPONSE_WAIT_DELAY_MIN = 2;
private const int RESPONSE_WAIT_DELAY_MAX = 5;
// "[EXP001000] Произошла ошибка при передаче данных. Попробуйте осуществить передачу данных повторно".
// Видимо, эту ошибку нельзя включать здесь. Предположительно это маркер DDOS защиты и если отправлять
// точно такой же пакет повторно, то ошибка входит в бесконечный цикл - необходимо заново
// собирать пакет с новыми кодами и временем и новой подписью. Такую ошибку надо обнаруживать
// на более высоком уровне и заново отправлять запрос новым пакетом.
private static readonly string[] ignorableSystemErrorMarkers = [
"Истекло время ожидания шлюза",
"Базовое соединение закрыто: Соединение, которое должно было работать, было разорвано сервером",
"Попробуйте осуществить передачу данных повторно",
"(502) Недопустимый шлюз",
"(503) Сервер не доступен"
];
protected ClientBase client;
protected CustomBinding binding;
protected abstract EndPoint EndPoint { get; }
/// <summary>
/// Для запросов, возвращающих мало данных, можно попробовать сократить
/// начальный период ожидания подготовки ответа
/// </summary>
protected abstract bool EnableMinimalResponseWaitDelay { get; }
/// <summary>
/// Указывает на то, что можно ли этот метод перезапускать в случае зависания
/// ожидания или в случае сбоя на сервере
/// </summary>
protected abstract bool CanBeRestarted { get; }
/// <summary>
/// Для противодействия зависанию ожидания вводится предел ожидания в минутах
/// для запросов, которые можно перезапустить заново с теми же параметрами
/// </summary>
protected abstract int RestartTimeoutMinutes { get; }
private EndpointAddress RemoteAddress => new(client.ComposeEndpointUri(EndPointLocator.GetPath(EndPoint)));
private string ThreadIdText => $"(Thread #{ThreadId})";
/// <summary>
/// Возвращает идентификатор текущего исполняемого потока
/// </summary>
private int ThreadId => Environment.CurrentManagedThreadId;
public RequestBase(ClientBase client)
{
this.client = client;
ConfigureBinding();
}
private void ConfigureBinding()
{
binding = new CustomBinding
{
CloseTimeout = TimeSpan.FromSeconds(180),
OpenTimeout = TimeSpan.FromSeconds(180),
ReceiveTimeout = TimeSpan.FromSeconds(180),
SendTimeout = TimeSpan.FromSeconds(180)
};
binding.Elements.Add(new TextMessageEncodingBindingElement
{
MessageVersion = MessageVersion.Soap11,
WriteEncoding = Encoding.UTF8
});
if (client.UseTunnel)
{
if (!System.Diagnostics.Process.GetProcessesByName("stunnel").Any())
{
throw new System.Exception("stunnel is not running");
}
binding.Elements.Add(new HttpTransportBindingElement
{
AuthenticationScheme = (client.IsPPAK ? System.Net.AuthenticationSchemes.Digest : System.Net.AuthenticationSchemes.Basic),
MaxReceivedMessageSize = int.MaxValue,
UseDefaultWebProxy = false
});
}
else
{
binding.Elements.Add(new HttpsTransportBindingElement
{
AuthenticationScheme = (client.IsPPAK ? System.Net.AuthenticationSchemes.Digest : System.Net.AuthenticationSchemes.Basic),
MaxReceivedMessageSize = int.MaxValue,
RequireClientCertificate = true,
UseDefaultWebProxy = false
});
}
}
protected async Task<TResult> SendAndWaitResultAsync(
object request,
Func<TAsyncClient, Task<TAck>> sender,
CancellationToken token)
{
token.ThrowIfCancellationRequested();
while (true)
{
try
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
var version = RequestHelper.GetRequestVersionString(request);
client.TryLog($"Executing request {RemoteAddress.Uri}/{request.GetType().Name} of version {version}...");
TAck ack;
var stopWatch = System.Diagnostics.Stopwatch.StartNew();
using (var asyncClient = CreateAsyncClient())
{
ack = await sender(asyncClient);
}
stopWatch.Stop();
client.TryLog($"Request executed in {stopWatch.ElapsedMilliseconds} ms, result GUID is {ack.MessageGUID}");
var result = await WaitForResultAsync(ack, true, token);
if (result is IQueryable queryableResult)
{
queryableResult.OfType<TErrorMessage>().ToList().ForEach(x =>
{
throw RemoteException.CreateNew(x.ErrorCode, x.Description);
});
}
else if (result is TErrorMessage x)
{
throw RemoteException.CreateNew(x.ErrorCode, x.Description);
}
return result;
}
catch (RestartTimeoutException e)
{
if (!CanBeRestarted)
{
throw new System.Exception("Cannot restart request execution on timeout", e);
}
client.TryLog($"Restarting {request.GetType().Name} request execution...");
}
}
}
private TAsyncClient CreateAsyncClient()
{
var asyncClient = (TAsyncClient)Activator.CreateInstance(typeof(TAsyncClient), binding, RemoteAddress);
ConfigureEndpointCredentials(asyncClient.Endpoint, asyncClient.ClientCredentials);
return asyncClient;
}
private void ConfigureEndpointCredentials(
ServiceEndpoint serviceEndpoint, ClientCredentials clientCredentials)
{
serviceEndpoint.EndpointBehaviors.Add(new GostSigningEndpointBehavior(client));
if (!client.IsPPAK)
{
clientCredentials.UserName.UserName = Constants.NAME_SIT;
clientCredentials.UserName.Password = Constants.PASSWORD_SIT;
}
System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate (
object sender, X509Certificate serverCertificate, X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
return true;
};
if (!client.UseTunnel)
{
clientCredentials.ClientCertificate.SetCertificate(
StoreLocation.CurrentUser,
StoreName.My,
X509FindType.FindByThumbprint,
client.CertificateThumbprint);
}
}
/// <summary>
/// Основной алгоритм ожидания ответа на асинхронный запрос.
/// Из документации ГИС ЖКХ:
/// Также рекомендуем придерживаться следующего алгоритма отправки запросов на получение статуса обработки пакета в случае использования асинхронных сервисов ГИС ЖКХ (в рамках одного MessageGUID):
/// - первый запрос getState направлять не ранее чем через 10 секунд, после получения квитанции о приеме пакета с бизнес-данными от сервиса ГИС КЖХ;
/// - в случае, если на первый запрос getSate получен результат с RequestState равным "1" или "2", то следующий запрос getState необходимо направлять не ранее чем через 60 секунд после отправки предыдущего запроса;
/// - в случае, если на второй запрос getSate получен результат с RequestState равным "1" или "2", то следующий запрос getState необходимо направлять не ранее чем через 300 секунд после отправки предыдущего запроса;
/// - в случае, если на третий запрос getSate получен результат с RequestState равным "1" или "2", то следующий запрос getState необходимо направлять не ранее чем через 900 секунд после отправки предыдущего запроса;
/// - в случае, если на четвертый(и все последующие запросы) getState получен результат с RequestState равным "1" или "2", то следующий запрос getState необходимо направлять не ранее чем через 1800 секунд после отправки предыдущего запроса.
/// </summary>
private async Task<TResult> WaitForResultAsync(
TAck ack, bool withInitialDelay, CancellationToken token)
{
TResult result;
var startTime = DateTime.Now;
for (var attempts = 1; ; attempts++)
{
token.ThrowIfCancellationRequested();
var delaySec = EnableMinimalResponseWaitDelay ? RESPONSE_WAIT_DELAY_MIN : RESPONSE_WAIT_DELAY_MAX;
if (attempts >= 2)
{
delaySec = RESPONSE_WAIT_DELAY_MAX;
}
if (attempts >= 3)
{
delaySec = RESPONSE_WAIT_DELAY_MAX * 2;
}
if (attempts >= 5)
{
delaySec = RESPONSE_WAIT_DELAY_MAX * 4;
}
if (attempts >= 7)
{
delaySec = RESPONSE_WAIT_DELAY_MAX * 8;
}
if (attempts >= 9)
{
delaySec = RESPONSE_WAIT_DELAY_MAX * 16;
}
if (attempts >= 12)
{
delaySec = RESPONSE_WAIT_DELAY_MAX * 60;
}
if (attempts > 1 || withInitialDelay)
{
var minutesElapsed = (int)(DateTime.Now - startTime).TotalMinutes;
if (CanBeRestarted && minutesElapsed > RestartTimeoutMinutes)
{
throw new RestartTimeoutException($"{RestartTimeoutMinutes} minute(s) wait time exceeded");
}
client.TryLog($"Waiting {delaySec} sec for attempt #{attempts}" +
$" to get response ({minutesElapsed} minute(s) elapsed)...");
await Task.Delay(delaySec * 1000, token);
}
client.TryLog($"Requesting response, attempt #{attempts} in {ThreadIdText}...");
result = await TryGetResultAsync(ack);
if (result != null)
{
break;
}
}
client.TryLog($"Response received!");
return result;
}
/// <summary>
/// Выполняет однократную проверку наличия результата.
/// Возвращает default если результата еще нет.
/// </summary>
private async Task<TResult> TryGetResultAsync(TAck ack)
{
using var asyncClient = CreateAsyncClient();
var requestHeader = RequestHelper.CreateHeader<TRequestHeader>(client);
var requestBody = new TGetStateRequest
{
MessageGUID = ack.MessageGUID
};
var response = await asyncClient.GetStateAsync(requestHeader, requestBody);
var result = response.GetStateResult;
if (result.RequestState == (int)AsyncRequestStateType.Ready)
{
return (TResult)result;
}
return default;
}
protected TRequestHeader CreateRequestHeader()
{
return RequestHelper.CreateHeader<TRequestHeader>(client);
}
/// <summary>
/// Исполнение повторяемой операции некоторое допустимое число ошибок
/// </summary>
protected async Task<TRepeatableResult> RunRepeatableTaskAsync<TRepeatableResult>(
Func<Task<TRepeatableResult>> taskFunc, Func<System.Exception, bool> canIgnoreFunc, int maxAttempts)
{
for (var attempts = 1; ; attempts++)
{
try
{
return await taskFunc();
}
catch (System.Exception e)
{
if (canIgnoreFunc(e))
{
if (attempts < maxAttempts)
{
client.TryLog($"Ignoring error of attempt #{attempts} of {maxAttempts} attempts");
continue;
}
throw new System.Exception("Too much attempts with error");
}
throw e;
}
}
}
/// <summary>
/// Для запросов к серверу которые можно направлять несколько раз, разрешаем
/// серверу аномально отказаться. Предполагается, что здесь мы игнорируем
/// только жесткие отказы серверной инфраструктуры, которые указывают
/// что запрос даже не был принят в обработку. Также все запросы на
/// чтение можно повторять в случае их серверных системных ошибок.
/// </summary>
protected async Task<TRepeatableResult> RunRepeatableTaskInsistentlyAsync<TRepeatableResult>(
Func<Task<TRepeatableResult>> func, CancellationToken token)
{
var afterErrorDelaySec = 120;
for (var attempt = 1; ; attempt++)
{
try
{
return await func();
}
catch (System.Exception e)
{
if (CanIgnoreSuchException(e, out string marker))
{
client.TryLog($"Ignoring error of attempt #{attempt} with type [{marker}]");
client.TryLog($"Waiting {afterErrorDelaySec} sec until next attempt...");
await Task.Delay(afterErrorDelaySec * 1000, token);
continue;
}
if (e is RestartTimeoutException)
{
throw e;
}
if (e is RemoteException)
{
throw RemoteException.CreateNew(e as RemoteException);
}
throw new System.Exception("Cannot ignore this exception", e);
}
}
}
private bool CanIgnoreSuchException(System.Exception e, out string resultMarker)
{
foreach (var marker in ignorableSystemErrorMarkers)
{
var found = Util.EnumerateInnerExceptions(e).Find(
x => x.Message != null && x.Message.Contains(marker));
if (found != null)
{
resultMarker = marker;
return true;
}
}
resultMarker = null;
return false;
}
}
}

View File

@ -0,0 +1,101 @@
using System;
using System.Linq;
namespace Hcs.Client.Api.Request
{
internal static class RequestHelper
{
/// <summary>
/// Подготовка заголовка сообщения отправляемого в ГИС ЖКХ с обязательными атрибутами.
/// Заголовки могут быть разного типа для разных типов сообщений, но имена полей одинаковые.
/// </summary>
internal static THeader CreateHeader<THeader>(ClientBase client) where THeader : class
{
try
{
var instance = Activator.CreateInstance(typeof(THeader));
foreach (var prop in instance.GetType().GetProperties())
{
switch (prop.Name)
{
case "Item":
prop.SetValue(instance, client.OrgPPAGUID);
break;
case "ItemElementName":
prop.SetValue(instance, Enum.Parse(prop.PropertyType, "orgPPAGUID"));
break;
case "MessageGUID":
prop.SetValue(instance, Guid.NewGuid().ToString());
break;
case "Date":
prop.SetValue(instance, DateTime.Now);
break;
case "IsOperatorSignatureSpecified":
if (client.Role == OrganizationRole.RC || client.Role == OrganizationRole.RSO)
{
prop.SetValue(instance, true);
}
break;
case "IsOperatorSignature":
if (client.Role == OrganizationRole.RC || client.Role == OrganizationRole.RSO)
{
prop.SetValue(instance, true);
}
break;
}
}
return instance as THeader;
}
catch (ArgumentNullException e)
{
throw new ApplicationException($"Error occured while building request header: {e.Message}");
}
catch (SystemException e)
{
throw new ApplicationException($"Error occured while building request header: {e.GetBaseException().Message}");
}
}
/// <summary>
/// Для объекта запроса возвращает значение строки свойства version
/// </summary>
internal static string GetRequestVersionString(object requestObject)
{
if (requestObject == null)
{
return null;
}
var versionHost = requestObject;
if (versionHost != null)
{
var versionProperty = versionHost.GetType().GetProperties().FirstOrDefault(x => x.Name == "version");
if (versionProperty != null)
{
return versionProperty.GetValue(versionHost) as string;
}
}
foreach (var field in requestObject.GetType().GetFields())
{
versionHost = field.GetValue(requestObject);
if (versionHost != null)
{
var versionProperty = versionHost.GetType().GetProperties().FirstOrDefault(x => x.Name == "version");
if (versionProperty != null)
{
return versionProperty.GetValue(versionHost) as string;
}
}
}
return null;
}
}
}

View File

@ -0,0 +1,172 @@
using Org.BouncyCastle.Asn1;
using System;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace Hcs.Client.Api.Request
{
internal class X509Tools
{
private const string PRIVATE_KEY_USAGE_PERIOD = "2.5.29.16";
public static string GetFullnameWithExpirationDateStr(X509Certificate2 x509cert)
{
var (фамилия, имя, отчество) = GetFullname(x509cert);
return фамилия + " " + имя + " " + отчество +
" до " + GetNotAfterDate(x509cert).ToString("dd.MM.yyyy");
}
/// <summary>
/// Возвращает массив из трех строк, содержащих соответственно Фамилию, Имя и Отчество
/// полученных из данных сертификата. Если сертификат не содержит ФИО, то возвращается массив
/// из трех пустых строк. Это не точный метод определять имя, он предполагает, что
/// поля SN, G, CN содержат ФИО в определенном порядке, что правдоподобно но не обязательно.
/// </summary>
private static (string Фамилия, string Имя, string Отчество) GetFullname(X509Certificate2 x509cert)
{
string фам = "", имя = "", отч = "";
// Сначала ищем поля surname (SN) и given-name (G)
var sn = DecodeSubjectField(x509cert, "SN");
var g = DecodeSubjectField(x509cert, "G");
if (!string.IsNullOrEmpty(sn) && !string.IsNullOrEmpty(g))
{
фам = sn;
var gParts = g.Split(' ');
if (gParts != null && gParts.Length >= 1)
{
имя = gParts[0];
}
if (gParts != null && gParts.Length >= 2)
{
отч = gParts[1];
}
}
else
{
// Иначе берем три первых слова из common name (CN), игнорируя кавычки
var cn = DecodeSubjectField(x509cert, "CN");
if (!string.IsNullOrEmpty(cn))
{
cn = new StringBuilder(cn).Replace("\"", "").ToString();
char[] separators = [' ', ';'];
var cnParts = cn.Split(separators);
if (cnParts != null && cnParts.Length >= 1)
{
фам = cnParts[0];
}
if (cnParts != null && cnParts.Length >= 2)
{
имя = cnParts[1];
}
if (cnParts != null && cnParts.Length >= 3)
{
отч = cnParts[2];
}
}
}
return (фам, имя, отч);
}
/// <summary>
/// Возвращает значение поля с именем @subName включенного в различимое имя Subject
/// </summary>
private static string DecodeSubjectField(X509Certificate2 x509cert, string subName)
{
// Чтобы посмотреть все поля сертификата
//System.Diagnostics.Trace.WriteLine("x509decode = " + x509cert.SubjectName.Decode(
//X500DistinguishedNameFlags.UseNewLines));
// Декодируем различимое имя на отдельные строки через переводы строк для надежности разбора
var decoded = x509cert.SubjectName.Decode(X500DistinguishedNameFlags.UseNewLines);
char[] separators = ['\n', '\r'];
var parts = decoded.Split(separators, StringSplitOptions.RemoveEmptyEntries);
if (parts == null)
{
return null;
}
// Каждая часть начинается с имени и отделяется от значения символом равно
foreach (var part in parts)
{
if (part.Length <= subName.Length + 1)
{
continue;
}
if (part.StartsWith(subName) && part[subName.Length] == '=')
{
return part.Substring(subName.Length + 1);
}
}
return null;
}
/// <summary>
/// Возвращает дату окончания действия сертификата
/// </summary>
private static DateTime GetNotAfterDate(X509Certificate2 x509cert)
{
// Сначала пытаемся определить срок первичного ключа, а затем уже самого сертификата
var датаОкончания = GetPrivateKeyUsageEndDate(x509cert);
if (датаОкончания != null)
{
return (DateTime)датаОкончания;
}
return x509cert.NotAfter;
}
private static DateTime? GetPrivateKeyUsageEndDate(X509Certificate2 x509cert)
{
foreach (var ext in x509cert.Extensions)
{
if (ext.Oid.Value == PRIVATE_KEY_USAGE_PERIOD)
{
// Дата начала с индексом 0, дата окончания с индексом 1
return ParseAsn1Datetime(ext, 1);
}
}
return null;
}
/// <summary>
/// Разбирает значение типа дата из серии значений ASN1 присоединенных к расширению
/// </summary>
private static DateTime? ParseAsn1Datetime(X509Extension ext, int valueIndex)
{
try
{
var asnObject = (new Asn1InputStream(ext.RawData)).ReadObject();
if (asnObject == null)
{
return null;
}
var asnSequence = Asn1Sequence.GetInstance(asnObject);
if (asnSequence.Count <= valueIndex)
{
return null;
}
var asn = (Asn1TaggedObject)asnSequence[valueIndex];
var asnStr = Asn1OctetString.GetInstance(asn, false);
var s = Encoding.UTF8.GetString(asnStr.GetOctets());
var year = int.Parse(s.Substring(0, 4));
var month = int.Parse(s.Substring(4, 2));
var day = int.Parse(s.Substring(6, 2));
var hour = int.Parse(s.Substring(8, 2));
var minute = int.Parse(s.Substring(10, 2));
var second = int.Parse(s.Substring(12, 2));
return new DateTime(year, month, day, hour, minute, second);
}
catch (System.Exception)
{
return null;
}
}
}
}