Refactor client classes
This commit is contained in:
7
Hcs.Client/Client/Api/ApiBase.cs
Normal file
7
Hcs.Client/Client/Api/ApiBase.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Hcs.Client.Api
|
||||
{
|
||||
public abstract class ApiBase(ClientBase client)
|
||||
{
|
||||
protected ClientBase client = client;
|
||||
}
|
||||
}
|
||||
32
Hcs.Client/Client/Api/NsiApi.cs
Normal file
32
Hcs.Client/Client/Api/NsiApi.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using Hcs.Client.Api.Request.Exception;
|
||||
using Hcs.Client.Api.Request.Nsi;
|
||||
using Hcs.Service.Async.Nsi;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Hcs.Client.Api
|
||||
{
|
||||
// http://open-gkh.ru/NsiService/
|
||||
public class NsiApi(ClientBase client) : ApiBase(client)
|
||||
{
|
||||
/// <summary>
|
||||
/// Возвращает данные справочника поставщика информации
|
||||
/// </summary>
|
||||
/// <param name="registryNumber">Реестровый номер справочника</param>
|
||||
/// <param name="token">Токен отмены</param>
|
||||
/// <returns>Данные справочника</returns>
|
||||
public async Task<IEnumerable<NsiItemType>> ExportDataProviderNsiItem(exportDataProviderNsiItemRequestRegistryNumber registryNumber, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new ExportDataProviderNsiItemRequest(client);
|
||||
return await request.ExecuteAsync(registryNumber, token);
|
||||
}
|
||||
catch (NoResultsRemoteException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Hcs.Client/Client/Api/Request/Adapter/IAck.cs
Normal file
9
Hcs.Client/Client/Api/Request/Adapter/IAck.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Hcs.Client.Api.Request
|
||||
{
|
||||
public interface IAck
|
||||
{
|
||||
string MessageGUID { get; set; }
|
||||
|
||||
string RequesterMessageGUID { get; set; }
|
||||
}
|
||||
}
|
||||
9
Hcs.Client/Client/Api/Request/Adapter/IAsyncClient.cs
Normal file
9
Hcs.Client/Client/Api/Request/Adapter/IAsyncClient.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
9
Hcs.Client/Client/Api/Request/Adapter/IErrorMessage.cs
Normal file
9
Hcs.Client/Client/Api/Request/Adapter/IErrorMessage.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Hcs.Client.Api.Request
|
||||
{
|
||||
public interface IErrorMessage
|
||||
{
|
||||
string ErrorCode { get; }
|
||||
|
||||
string Description { get; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Hcs.Client.Api.Request
|
||||
{
|
||||
public interface IGetStateRequest
|
||||
{
|
||||
string MessageGUID { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Hcs.Client.Api.Request.Adapter
|
||||
{
|
||||
public interface IGetStateResponse
|
||||
{
|
||||
IGetStateResult GetStateResult { get; }
|
||||
}
|
||||
}
|
||||
7
Hcs.Client/Client/Api/Request/Adapter/IGetStateResult.cs
Normal file
7
Hcs.Client/Client/Api/Request/Adapter/IGetStateResult.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Hcs.Client.Api.Request.Adapter
|
||||
{
|
||||
public interface IGetStateResult
|
||||
{
|
||||
sbyte RequestState { get; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Hcs.Client.Api.Request.Adapter
|
||||
{
|
||||
public interface IGetStateResultMany : IGetStateResult
|
||||
{
|
||||
object[] Items { get; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Hcs.Client.Api.Request.Adapter
|
||||
{
|
||||
public interface IGetStateResultOne : IGetStateResult
|
||||
{
|
||||
object Item { get; }
|
||||
}
|
||||
}
|
||||
9
Hcs.Client/Client/Api/Request/AsyncRequestStateType.cs
Normal file
9
Hcs.Client/Client/Api/Request/AsyncRequestStateType.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Hcs.Client.Api.Request
|
||||
{
|
||||
internal enum AsyncRequestStateType
|
||||
{
|
||||
Received = 1,
|
||||
InProgress,
|
||||
Ready
|
||||
}
|
||||
}
|
||||
16
Hcs.Client/Client/Api/Request/EndPoint.cs
Normal file
16
Hcs.Client/Client/Api/Request/EndPoint.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace Hcs.Client.Api.Request
|
||||
{
|
||||
internal enum EndPoint
|
||||
{
|
||||
BillsAsync,
|
||||
DebtRequestsAsync,
|
||||
DeviceMeteringAsync,
|
||||
HomeManagementAsync,
|
||||
LicensesAsync,
|
||||
NsiAsync,
|
||||
NsiCommonAsync,
|
||||
OrgRegistryAsync,
|
||||
OrgRegistryCommonAsync,
|
||||
PaymentsAsync
|
||||
}
|
||||
}
|
||||
30
Hcs.Client/Client/Api/Request/EndPointLocator.cs
Normal file
30
Hcs.Client/Client/Api/Request/EndPointLocator.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) { }
|
||||
}
|
||||
}
|
||||
65
Hcs.Client/Client/Api/Request/Exception/RemoteException.cs
Normal file
65
Hcs.Client/Client/Api/Request/Exception/RemoteException.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) { }
|
||||
}
|
||||
}
|
||||
23
Hcs.Client/Client/Api/Request/GostSigningEndpointBehavior.cs
Normal file
23
Hcs.Client/Client/Api/Request/GostSigningEndpointBehavior.cs
Normal 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) { }
|
||||
}
|
||||
}
|
||||
116
Hcs.Client/Client/Api/Request/GostSigningMessageInspector.cs
Normal file
116
Hcs.Client/Client/Api/Request/GostSigningMessageInspector.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
49
Hcs.Client/Client/Api/Request/Nsi/NsiRequestBase.cs
Normal file
49
Hcs.Client/Client/Api/Request/Nsi/NsiRequestBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
407
Hcs.Client/Client/Api/Request/RequestBase.cs
Normal file
407
Hcs.Client/Client/Api/Request/RequestBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
101
Hcs.Client/Client/Api/Request/RequestHelper.cs
Normal file
101
Hcs.Client/Client/Api/Request/RequestHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
172
Hcs.Client/Client/Api/Request/X509Tools.cs
Normal file
172
Hcs.Client/Client/Api/Request/X509Tools.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user