Add project

Basic formatting applied. Unnecessary comments have been removed. Suspicious code is covered by TODO.
This commit is contained in:
2025-08-12 11:21:10 +09:00
parent bbcbe841a7
commit 33ab055b43
546 changed files with 176950 additions and 0 deletions

View File

@ -0,0 +1,34 @@
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
namespace Hcs.ClientApi.RemoteCaller
{
public class GostSigningEndpointBehavior : IEndpointBehavior
{
private HcsClientConfig clientConfig;
public GostSigningEndpointBehavior(HcsClientConfig clientConfig)
{
this.clientConfig = clientConfig;
}
public void Validate(ServiceEndpoint endpoint)
{
}
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(
new GostSigningMessageInspector(clientConfig));
}
}
}

View File

@ -0,0 +1,120 @@
using Hcs.GostXades;
using System;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Text;
using System.Xml;
namespace Hcs.ClientApi.RemoteCaller
{
/// <summary>
/// Фильтр сообщений добавляет в XML сообщение электронную подпись XADES/GOST.
/// </summary>
internal class GostSigningMessageInspector : IClientMessageInspector
{
private HcsClientConfig clientConfig;
public GostSigningMessageInspector(HcsClientConfig clientConfig)
{
this.clientConfig = clientConfig;
}
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
try
{
string filterHeader = " Фильтр отправки:";
PurgeDebuggerHeaders(ref request);
var messageBody = GetMessageBodyString(ref request, Encoding.UTF8);
if (!messageBody.Contains(HcsConstants.SignedXmlElementId))
{
clientConfig.MaybeCaptureMessage(true, messageBody);
}
else
{
string certInfo = HcsX509Tools.ДатьСтрокуФИОСертификатаСДатойОкончания(clientConfig.Certificate);
clientConfig.Log($"{filterHeader} подписываю сообщение ключем [{certInfo}]...");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var service = new GostXadesBesService(clientConfig.CryptoProviderType);
var signedXml = service.Sign(messageBody,
HcsConstants.SignedXmlElementId,
clientConfig.CertificateThumbprint,
clientConfig.CertificatePassword);
stopwatch.Stop();
clientConfig.Log($"{filterHeader} сообщение подписано за {stopwatch.ElapsedMilliseconds}мс.");
clientConfig.MaybeCaptureMessage(true, signedXml);
request = Message.CreateMessage(
XmlReaderFromString(signedXml), int.MaxValue, request.Version);
}
}
catch (Exception ex)
{
string error = $"В {GetType().Name} произошло исключение";
throw new Exception(error, ex);
}
return null;
}
public void AfterReceiveReply(ref Message reply, object correlationState)
{
clientConfig.MaybeCaptureMessage(false, reply.ToString());
}
private void PurgeDebuggerHeaders(ref Message request)
{
int limit = request.Headers.Count;
for (int i = 0; i < limit; ++i)
{
if (request.Headers[i].Name.Equals("VsDebuggerCausalityData"))
{
request.Headers.RemoveAt(i);
break;
}
}
}
string GetMessageBodyString(ref Message request, Encoding encoding)
{
MessageBuffer mb = request.CreateBufferedCopy(int.MaxValue);
request = mb.CreateMessage();
Stream s = new MemoryStream();
XmlWriter xw = XmlWriter.Create(s);
mb.CreateMessage().WriteMessage(xw);
xw.Flush();
s.Position = 0;
byte[] 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);
}
}
XmlReader XmlReaderFromString(String xml)
{
var stream = new MemoryStream();
var writer = new System.IO.StreamWriter(stream);
writer.Write(xml);
writer.Flush();
stream.Position = 0;
return XmlReader.Create(stream);
}
}
}

View File

@ -0,0 +1,66 @@
using System;
namespace Hcs.ClientApi.RemoteCaller
{
/// <summary>
/// Состояние многостраничной выдачи для методов HCS выдыющих длинные списки.
/// Списки выдаются порциями по 100 позиций и в каждой порции указано состояние
/// многостраничной выдачи одним значением - это либо bool со значением true что
/// означает что эта порция последняя IsLastPage, либо это строка содержащая
/// guid объекта начала следующей порции - и этот guid надо указать в запросе
/// чтобы получить следующую порцию.
/// </summary>
public class HcsPagedResultState
{
/// <summary>
/// Состояние указыввает что это последняя страница
/// </summary>
public bool IsLastPage { get; private set; }
/// <summary>
/// Состояние указывает что это не последняя страница и
/// следующая страница начинается с NextGuid
/// </summary>
public Guid NextGuid { get; private set; }
private const string me = nameof(HcsPagedResultState);
public static readonly HcsPagedResultState IsLastPageResultState = new HcsPagedResultState(true);
/// <summary>
/// Новый маркер состояния многостраничной выдачи метода HCS
/// </summary>
public HcsPagedResultState(object item)
{
if (item == null) throw new HcsException($"{me}.Item is null");
if (item is bool)
{
if ((bool)item == false) throw new HcsException($"{me}.IsLastPage is false");
IsLastPage = true;
}
else if (item is string)
{
try
{
IsLastPage = false;
NextGuid = HcsUtil.ParseGuid((string)item);
}
catch (Exception e)
{
throw new HcsException($"Failed to parse {me}.NextGuid value", e);
}
}
else
{
throw new HcsException($"{me}.Item is of unrecognized type " + item.GetType().FullName);
}
}
public override string ToString()
{
return $"{me}({nameof(IsLastPage)}={IsLastPage}" +
(IsLastPage ? "" : $",{nameof(NextGuid)}={NextGuid}") + ")";
}
}
}

View File

@ -0,0 +1,373 @@
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.ClientApi.RemoteCaller
{
/// <summary>
/// Базовый класс для методов HCS вызываемых удаленно
/// </summary>
public abstract class HcsRemoteCallMethod
{
public HcsClientConfig _config;
protected CustomBinding _binding;
/// <summary>
/// Для методов возвращающих мало данных можно попробовать сократить
/// начальный период ожидания подготовки ответа
/// </summary>
public bool EnableMinimalResponseWaitDelay { get; internal set; }
/// <summary>
/// Для противодействия зависанию ожидания вводится предел ожидания в минутах
/// для методов которые можно перезапустить заново с теми же параметрами.
/// С периодом в 120 минут 09.2024 не успевали за ночь получить все данные.
/// </summary>
public int RestartTimeoutMinutes = 20;
/// <summary>
/// Можно ли этот метод перезапускать в случае зависания ожидания или в случае сбоя на сервере?
/// </summary>
public bool CanBeRestarted { get; protected set; }
public HcsClientConfig ClientConfig => _config;
public HcsRemoteCallMethod(HcsClientConfig config)
{
this._config = config;
ConfigureBinding();
}
private void ConfigureBinding()
{
_binding = new CustomBinding();
// Эксперимент 19.07.2022 возникает ошибка WCF (TimeoutException 60 сек)
_binding.ReceiveTimeout = TimeSpan.FromSeconds(180);
_binding.OpenTimeout = TimeSpan.FromSeconds(180);
_binding.SendTimeout = TimeSpan.FromSeconds(180);
_binding.CloseTimeout = TimeSpan.FromSeconds(180);
_binding.Elements.Add(new TextMessageEncodingBindingElement
{
MessageVersion = MessageVersion.Soap11,
WriteEncoding = Encoding.UTF8
});
if (_config.UseTunnel)
{
if (System.Diagnostics.Process.GetProcessesByName("stunnel").Any() ? false : true)
{
throw new Exception("stunnel не запущен");
}
_binding.Elements.Add(new HttpTransportBindingElement
{
AuthenticationScheme = (_config.IsPPAK ? System.Net.AuthenticationSchemes.Digest : System.Net.AuthenticationSchemes.Basic),
MaxReceivedMessageSize = int.MaxValue,
UseDefaultWebProxy = false
});
}
else
{
_binding.Elements.Add(new HttpsTransportBindingElement
{
AuthenticationScheme = (_config.IsPPAK ? System.Net.AuthenticationSchemes.Digest : System.Net.AuthenticationSchemes.Basic),
MaxReceivedMessageSize = int.MaxValue,
UseDefaultWebProxy = false,
RequireClientCertificate = true
});
}
}
protected EndpointAddress GetEndpointAddress(string endpointName)
{
return new EndpointAddress(_config.ComposeEndpointUri(endpointName));
}
protected void ConfigureEndpointCredentials(
ServiceEndpoint serviceEndpoint, ClientCredentials clientCredentials)
{
serviceEndpoint.EndpointBehaviors.Add(new GostSigningEndpointBehavior(_config));
if (!_config.IsPPAK)
{
clientCredentials.UserName.UserName = HcsConstants.UserAuth.Name;
clientCredentials.UserName.Password = HcsConstants.UserAuth.Passwd;
System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate (
object sender, X509Certificate serverCertificate, X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
return true;
};
}
else
{
bool letSystemValidateServerCertificate = false;
if (!letSystemValidateServerCertificate)
{
System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate (
object sender, X509Certificate serverCertificate, X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
// 06.06.2024 возникла ошибка "Это может быть связано с тем, что сертификат сервера
// не настроен должным образом с помощью HTTP.SYS в случае HTTPS."
// ГИС ЖКХ заменил сертификат сервера HTTPS и System.Net не смогла проверить новый.
// В похожем случае необходимо включить "return true" чтобы любой сертификат
// без проверки принимался (или найти файл lk_api_dom_gosuslugi_ru.cer нового сертификата
// сервера ГИС ЖКХ API в разделе "Регламенты и инструкции" портала dom.gosuslugi.ru
// и установить этот сертификат текущему пользователю).
// Файл сертификата сервера API в разделе "Регламенты и инструкции" называется, например, так:
// "Сертификат открытого ключа для организации защищенного TLS соединения с сервисами
// легковесной интеграции (c 10.06.2024)".
return true;
};
}
}
if (!_config.UseTunnel)
{
clientCredentials.ClientCertificate.SetCertificate(
StoreLocation.CurrentUser,
StoreName.My,
X509FindType.FindByThumbprint,
_config.CertificateThumbprint);
}
}
/// <summary>
/// Выполнение одной попытки пооучить результат операции.
/// Реализуется в производных классах.
/// </summary>
protected abstract Task<IHcsGetStateResult> TryGetResultAsync(IHcsAck sourceAck, CancellationToken token);
/// <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>
protected async Task<IHcsGetStateResult> WaitForResultAsync(
IHcsAck ack, bool withInitialDelay, CancellationToken token)
{
var startTime = DateTime.Now;
IHcsGetStateResult result;
for (int attempts = 1; ; attempts++)
{
token.ThrowIfCancellationRequested();
int delaySec = EnableMinimalResponseWaitDelay ? 2 : 5;
if (attempts >= 2) delaySec = 5;
if (attempts >= 3) delaySec = 10;
if (attempts >= 5) delaySec = 20;
if (attempts >= 7) delaySec = 40;
if (attempts >= 9) delaySec = 80;
if (attempts >= 12) delaySec = 300;
if (attempts > 1 || withInitialDelay)
{
var minutesElapsed = (int)(DateTime.Now - startTime).TotalMinutes;
if (CanBeRestarted && minutesElapsed > RestartTimeoutMinutes)
throw new HcsRestartTimeoutException($"Превышено ожидание в {RestartTimeoutMinutes} минут");
_config.Log($"Ожидаю {delaySec} сек. до попытки #{attempts}" +
$" получить ответ (ожидание {minutesElapsed} минут(ы))...");
await Task.Delay(delaySec * 1000, token);
}
_config.Log($"Запрашиваю ответ, попытка #{attempts} {ThreadIdText}...");
result = await TryGetResultAsync(ack, token);
if (result != null) break;
}
_config.Log($"Ответ получен, число частей: {result.Items.Count()}");
return result;
}
/// <summary>
/// Исполнение повторяемой операции некоторое дпустимое число ошибок
/// </summary>
public async Task<T> RunRepeatableTaskAsync<T>(
Func<Task<T>> taskFunc, Func<Exception, bool> canIgnoreFunc, int maxAttempts)
{
for (int attempts = 1; ; attempts++)
{
try
{
return await taskFunc();
}
catch (Exception e)
{
if (canIgnoreFunc(e))
{
if (attempts < maxAttempts)
{
Log($"Игнорирую {attempts} из {maxAttempts} допустимых ошибок");
continue;
}
throw new HcsException(
$"Более {maxAttempts} продолжений после допустимых ошибок", e);
}
throw new HcsException("Вложенная ошибка", e);
}
}
}
/// <summary>
/// Для запросов к серверу которые можно направлять несколько раз, разрешаем
/// серверу аномально отказаться. Предполагается, что здесь мы игнорируем
/// только жесткие отказы серверной инфраструктуры, которые указывают
/// что запрос даже не был принят в обработку. Также все запросы на
/// чтение можно повторять в случае их серверных системных ошибок.
/// </summary>
protected async Task<T> RunRepeatableTaskInsistentlyAsync<T>(
Func<Task<T>> func, CancellationToken token)
{
int afterErrorDelaySec = 120;
for (int attempt = 1; ; attempt++)
{
try
{
return await func();
}
catch (Exception e)
{
string marker;
if (CanIgnoreSuchException(e, out marker))
{
_config.Log($"Игнорирую ошибку #{attempt} типа [{marker}].");
_config.Log($"Ожидаю {afterErrorDelaySec} сек. до повторения после ошибки...");
await Task.Delay(afterErrorDelaySec * 1000, token);
continue;
}
if (e is HcsRestartTimeoutException)
throw new HcsRestartTimeoutException("Наступило событие рестарта", e);
// Ошибки удаленной системы, которые нельзя игнорировать, дублируем для точности перехвата
if (e is HcsRemoteException) throw HcsRemoteException.CreateNew(e as HcsRemoteException);
throw new HcsException("Ошибка, которую нельзя игнорировать", e);
}
}
}
// "[EXP001000] Произошла ошибка при передаче данных. Попробуйте осуществить передачу данных повторно",
// Видимо, эту ошибку нельзя включать здесь. Предположительно это маркер DDOS защиты и если отправлять
// точно такой же пакет повторно, то ошибка входит в бесконечный цикл - необходимо заново
// собирать пакет с новыми кодами и временем и новой подписью. Такую ошибку надо обнаруживать
// на более высоком уровне и заново отправлять запрос новым пакетом. (21.09.2022)
private static string[] ignorableSystemErrorMarkers = {
"Истекло время ожидания шлюза",
"Базовое соединение закрыто: Соединение, которое должно было работать, было разорвано сервером",
"Попробуйте осуществить передачу данных повторно", // Включено 18.10.2024, HouseManagement API сильно сбоит
"(502) Недопустимый шлюз",
"(503) Сервер не доступен"
};
private bool CanIgnoreSuchException(Exception e, out string resultMarker)
{
foreach (var marker in ignorableSystemErrorMarkers)
{
var found = HcsUtil.EnumerateInnerExceptions(e).Find(
x => x.Message != null && x.Message.Contains(marker));
if (found != null)
{
resultMarker = marker;
return true;
}
}
resultMarker = null;
return false;
}
/// <summary>
/// Проверяет массив @items на содержание строго одного элемента типа @T и этот элемент
/// </summary>
protected T RequireSingleItem<T>(object[] items)
{
if (items == null)
throw new HcsException($"Array of type {typeof(T)} must not be null");
if (items.Length == 0)
throw new HcsException($"Array of type {typeof(T)} must not be empty");
if (items.Length > 1)
throw new HcsException($"Array of type {typeof(T)} must contain 1 element, not {items.Length} of type {items[0].GetType().FullName}");
return RequireType<T>(items[0]);
}
/// <summary>
/// Проверяет @obj на соответствие типу @T и возвращает преобразованный объект
/// </summary>
protected T RequireType<T>(object obj)
{
if (obj != null)
{
if (typeof(T) == obj.GetType()) return (T)obj;
}
throw new HcsException(
$"Require object of type {typeof(T)} but got" +
(obj == null ? "null" : obj.GetType().FullName));
}
internal static HcsException NewUnexpectedObjectException(object obj)
{
if (obj == null) return new HcsException("unexpected object is null");
return new HcsException($"Unexpected object [{obj}] of type {obj.GetType().FullName}");
}
public static string FormatGuid(Guid guid) => HcsUtil.FormatGuid(guid);
public static string FormatGuid(Guid? guid) => (guid != null) ? FormatGuid((Guid)guid) : null;
public static Guid ParseGuid(string guid) => HcsUtil.ParseGuid(guid);
public static Guid ParseGuid(object obj)
{
if (obj == null) throw new HcsException("Can't parse null as Guid");
if (obj is Guid) return (Guid)obj;
return ParseGuid(obj.ToString());
}
public static Guid[] ParseGuidArray(string[] array)
{
if (array == null) return null;
return array.ToList().Select(x => ParseGuid(x)).ToArray();
}
public bool IsArrayEmpty(Array a) => (a == null || a.Length == 0);
public string MakeEmptyNull(string s)
{
return string.IsNullOrEmpty(s) ? null : s;
}
/// <summary>
/// Выполняет @action на объекте @x если объект не пустой и приводится к типу @T
/// </summary>
public void CallOnType<T>(object x, Action<T> action) where T : class
{
var t = x as T;
if (t != null) action(t);
}
/// <summary>
/// Возвращает индентификатор текущего исполняемого потока
/// </summary>
public int ThreadId => System.Environment.CurrentManagedThreadId;
public string ThreadIdText => $"(thread #{ThreadId})";
public void Log(string message) => ClientConfig.Log(message);
}
}

View File

@ -0,0 +1,84 @@
using System;
using System.Linq;
namespace Hcs.ClientApi.RemoteCaller
{
public static class HcsRequestHelper
{
/// <summary>
/// Подготовка заголовка сообщения отправляемого в ГИС ЖКХ с обязательными атрибутами.
/// Заголовки могут быть разного типа для разных типов сообщений но имена полей одинаковые.
/// </summary>
public static THeaderType CreateHeader<THeaderType>(HcsClientConfig config) where THeaderType : class
{
try
{
var instance = Activator.CreateInstance(typeof(THeaderType));
foreach (var prop in instance.GetType().GetProperties())
{
switch (prop.Name)
{
case "Item":
prop.SetValue(instance, config.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 (config.Role == HcsOrganizationRoles.RC || config.Role == HcsOrganizationRoles.RSO)
prop.SetValue(instance, true);
break;
case "IsOperatorSignature":
if (config.Role == HcsOrganizationRoles.RC || config.Role == HcsOrganizationRoles.RSO)
prop.SetValue(instance, true);
break;
}
}
return instance as THeaderType;
}
catch (ArgumentNullException ex)
{
throw new ApplicationException($"При сборке заголовка запроса для ГИС произошла ошибка: {ex.Message}");
}
catch (SystemException exc)
{
throw new ApplicationException($"При сборке заголовка запроса для ГИС произошла не предвиденная ошибка {exc.GetBaseException().Message}");
}
}
/// <summary>
/// Для объекта запроса возвращает значение строки свойства version
/// </summary>
public static string GetRequestVersionString(object requestObject)
{
if (requestObject == null) return null;
object 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,18 @@
namespace Hcs.ClientApi.RemoteCaller
{
/// <summary>
/// Конфигурация ServicePointManager для работы с TLS. Скорее всего класс не нужен.
/// </summary>
public static class HcsServicePointConfig
{
public static void InitConfig()
{
// TODO: Проверить комментарий
// Отключено 15.12.2023, работает и так
//ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
//ServicePointManager.CheckCertificateRevocationList = false;
//ServicePointManager.ServerCertificateValidationCallback = ((sender, certificate, chain, sslPolicyErrors) => true);
//ServicePointManager.Expect100Continue = false;
}
}
}

View File

@ -0,0 +1,8 @@
namespace Hcs.ClientApi.RemoteCaller
{
public interface IHcsAck
{
string MessageGUID { get; set; }
string RequesterMessageGUID { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace Hcs.ClientApi.RemoteCaller
{
public interface IHcsFault
{
string ErrorCode { get; set; }
string ErrorMessage { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace Hcs.ClientApi.RemoteCaller
{
public interface IHcsGetStateResult
{
object[] Items { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Hcs.ClientApi.RemoteCaller
{
public interface IHcsHeaderType
{
string MessageGUID { get; set; }
DateTime Date { get; set; }
}
}