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); } }