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
{
///
/// Базовый класс для методов HCS вызываемых удаленно
///
public abstract class HcsRemoteCallMethod where T : IHcsGetStateResult
{
public HcsClientConfig _config;
protected CustomBinding _binding;
///
/// Для методов возвращающих мало данных можно попробовать сократить
/// начальный период ожидания подготовки ответа
///
public bool EnableMinimalResponseWaitDelay { get; internal set; }
///
/// Для противодействия зависанию ожидания вводится предел ожидания в минутах
/// для методов которые можно перезапустить заново с теми же параметрами.
/// С периодом в 120 минут 09.2024 не успевали за ночь получить все данные.
///
public int RestartTimeoutMinutes = 20;
///
/// Можно ли этот метод перезапускать в случае зависания ожидания или в случае сбоя на сервере?
///
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);
}
}
///
/// Выполнение одной попытки пооучить результат операции.
/// Реализуется в производных классах.
///
protected abstract Task TryGetResultAsync(IHcsAck sourceAck, CancellationToken token);
///
/// Основной алгоритм ожидания ответа на асинхронный запрос.
/// Из документации ГИС ЖКХ:
/// Также рекомендуем придерживаться следующего алгоритма отправки запросов на получение статуса обработки пакета в случае использования асинхронных сервисов ГИС ЖКХ (в рамках одного 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 секунд после отправки предыдущего запроса.
///
protected async Task WaitForResultAsync(
IHcsAck ack, bool withInitialDelay, CancellationToken token)
{
var startTime = DateTime.Now;
T 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($"Ответ получен");
return result;
}
///
/// Исполнение повторяемой операции некоторое дпустимое число ошибок
///
public async Task RunRepeatableTaskAsync(
Func> taskFunc, Func 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);
}
}
}
///
/// Для запросов к серверу которые можно направлять несколько раз, разрешаем
/// серверу аномально отказаться. Предполагается, что здесь мы игнорируем
/// только жесткие отказы серверной инфраструктуры, которые указывают
/// что запрос даже не был принят в обработку. Также все запросы на
/// чтение можно повторять в случае их серверных системных ошибок.
///
protected async Task RunRepeatableTaskInsistentlyAsync(
Func> 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;
}
///
/// Проверяет массив @items на содержание строго одного элемента типа @T и этот элемент
///
protected T RequireSingleItem(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(items[0]);
}
///
/// Проверяет @obj на соответствие типу @T и возвращает преобразованный объект
///
protected T RequireType(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;
}
///
/// Выполняет @action на объекте @x если объект не пустой и приводится к типу @T
///
public void CallOnType(object x, Action action) where T : class
{
var t = x as T;
if (t != null) action(t);
}
///
/// Возвращает индентификатор текущего исполняемого потока
///
public int ThreadId => System.Environment.CurrentManagedThreadId;
public string ThreadIdText => $"(thread #{ThreadId})";
public void Log(string message) => ClientConfig.Log(message);
}
}