Вынести условия-предикаты в extension-методы
HasUser из 13 endpoint-файлов сведён в AuthHelpers.HasUser; 15 приватных предикатов заменены extension-методами с удалением дублирующих приватных методов: IsCommunicationFailure, IsTransportFailure, IsPrivateEndpoint, IsConfigured, IsTrue, IsExpired, IsFailedLogin/IsSuccessfulLogin, HasChanges, HasBudget, HasAnyTerm, IsCurrencyLetter, IsEmojiCodePoint, ContainsFooterHint, IsTypeLabel.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using Deal.Modules.Settings.Application;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
@@ -128,7 +127,7 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
|
||||
// Private/loopback/link-local литералы и localhost запрещены для не-local провайдеров (локальные
|
||||
// провайдеры — ветка IsLocal выше, HTTP для них не выполняется вовсе). DNS-имена не резолвятся
|
||||
// здесь (полный egress-контроль с резолвом — на уровне сетевого периметра/прокси).
|
||||
if (IsPrivateEndpoint(modelsUri))
|
||||
if (modelsUri.IsPrivateEndpoint())
|
||||
{
|
||||
return BuildResult(request, name, ok: false, message: PrivateEndpointNotAllowedMessage);
|
||||
}
|
||||
@@ -247,50 +246,8 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
}
|
||||
|
||||
// Проверяет, указывает ли URL на приватный/loopback/link-local адрес (SSRF-гейт).
|
||||
// Распознаются IP-литералы (IPv4/IPv6) и имя localhost; DNS-имена считаются публичными
|
||||
// (полный egress-контроль с резолвом выполняется на сетевом периметре).
|
||||
// uri: Абсолютный http(s)-адрес.
|
||||
// Возвращает: True — адрес приватный/локальный (HTTP к нему запрещён).
|
||||
private static bool IsPrivateEndpoint(Uri uri)
|
||||
{
|
||||
string host = uri.Host;
|
||||
if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IPAddress.TryParse(host, out IPAddress? address))
|
||||
{
|
||||
return false; // DNS-имя — резолв вне этого слоя
|
||||
}
|
||||
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
if (IPAddress.IsLoopback(address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
{
|
||||
byte[] bytes = address.GetAddressBytes();
|
||||
return bytes[0] == 10
|
||||
|| (bytes[0] == 172 && bytes[1] is >= 16 and <= 31)
|
||||
|| (bytes[0] == 192 && bytes[1] == 168)
|
||||
|| bytes[0] == 169 && bytes[1] == 254 // link-local (включая 169.254.169.254 metadata)
|
||||
|| bytes[0] == 0;
|
||||
}
|
||||
|
||||
// IPv6: уникальные локальные (fc00::/7) и link-local (fe80::/10).
|
||||
byte[] v6 = address.GetAddressBytes();
|
||||
return (v6[0] & 0xFE) == 0xFC || (v6[0] == 0xFE && (v6[1] & 0xC0) == 0x80);
|
||||
}
|
||||
/// <param name="exception">Исключение HTTP-слоя.</param>
|
||||
/// <returns>Человекочитаемый текст причины.</returns>
|
||||
// Исключение HTTP-слоя.
|
||||
// Возвращает: Человекочитаемый текст причины.
|
||||
private static string ConnectionErrorDetail(HttpRequestException exception)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(exception.Message))
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Grpc.Core;
|
||||
|
||||
namespace Deal.Infrastructure.Integrations;
|
||||
|
||||
/// <summary>
|
||||
/// Расширения классификации gRPC-исключений клиентов автономных сервисов.
|
||||
/// </summary>
|
||||
internal static class RpcExceptionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Ошибки коммуникации, при которых сервис считается недоступным (всё, кроме прикладных статусов).
|
||||
/// </summary>
|
||||
/// <param name="exception">Исключение RPC.</param>
|
||||
/// <returns>True — транспорта/контракта health нет (down); false — прикладной статус (не наша зона).</returns>
|
||||
public static bool IsCommunicationFailure(this RpcException exception) =>
|
||||
exception.StatusCode is StatusCode.Unavailable or StatusCode.DeadlineExceeded or StatusCode.Unimplemented;
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public sealed class ServiceHealthProbe
|
||||
Reachable: true,
|
||||
Serving: response.Status == HealthCheckResponse.Types.ServingStatus.Serving);
|
||||
}
|
||||
catch (RpcException exception) when (IsCommunicationFailure(exception))
|
||||
catch (RpcException exception) when (exception.IsCommunicationFailure())
|
||||
{
|
||||
return ServiceHealthResult.Unreachable;
|
||||
}
|
||||
@@ -73,10 +73,4 @@ public sealed class ServiceHealthProbe
|
||||
return ServiceHealthResult.Unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
// Ошибки коммуникации, при которых сервис считается недоступным (всё, кроме прикладных статусов).
|
||||
// exception: Исключение RPC.
|
||||
// Возвращает: True — транспорта/контракта health нет (down), false — прикладной статус (не наша зона).
|
||||
private static bool IsCommunicationFailure(RpcException exception) =>
|
||||
exception.StatusCode is StatusCode.Unavailable or StatusCode.DeadlineExceeded or StatusCode.Unimplemented;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public static class FileStorageRegistrar
|
||||
|
||||
// MinIO-режим: только когда секция/алиасы заполнены (Ruling 4: «заглушка-адаптер, если MinIO
|
||||
// недоступен» — dev/curl/unit по умолчанию работают на LocalFileStorage без MinIO).
|
||||
if (MinioConfigured(options.Minio))
|
||||
if (options.Minio.IsConfigured())
|
||||
{
|
||||
services.AddSingleton<IFileStorage>(serviceProvider =>
|
||||
new MinioFileStorage(options.Minio, serviceProvider.GetRequiredService<ILogger<MinioFileStorage>>()));
|
||||
@@ -113,20 +113,12 @@ public static class FileStorageRegistrar
|
||||
|
||||
if (secureRaw is not null)
|
||||
{
|
||||
options.Minio.Secure = IsTrue(secureRaw);
|
||||
options.Minio.Secure = secureRaw.IsTrue();
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// True — секция Minio заполнена настолько, что возможен Minio-адаптер (Ruling 4: Endpoint + креды).
|
||||
private static bool MinioConfigured(MinioStorageOptions minio)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(minio.Endpoint)
|
||||
&& !string.IsNullOrWhiteSpace(minio.AccessKey)
|
||||
&& !string.IsNullOrWhiteSpace(minio.SecretKey);
|
||||
}
|
||||
|
||||
// Резолвит корень локального хранилища: дефолт data/attachments под ContentRoot; относительный Root — под ContentRoot; абсолютный — как есть.
|
||||
private static string ResolveLocalRoot(LocalStorageOptions local, string contentRootPath)
|
||||
{
|
||||
@@ -172,8 +164,4 @@ public static class FileStorageRegistrar
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsTrue(string raw)
|
||||
{
|
||||
return string.Equals(raw, "true", StringComparison.OrdinalIgnoreCase) || raw == "1";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Deal.Infrastructure.Integrations.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Расширения <see cref="MinioStorageOptions"/> (выбор MinIO-адаптера по заполненности секции).
|
||||
/// </summary>
|
||||
internal static class MinioStorageOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// True — секция Minio заполнена настолько, что возможен Minio-адаптер (Ruling 4: Endpoint + креды).
|
||||
/// </summary>
|
||||
/// <param name="minio">Настройки MinIO из секции Storage:Minio.</param>
|
||||
/// <returns>True — заданы Endpoint, AccessKey и SecretKey.</returns>
|
||||
public static bool IsConfigured(this MinioStorageOptions minio)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(minio.Endpoint)
|
||||
&& !string.IsNullOrWhiteSpace(minio.AccessKey)
|
||||
&& !string.IsNullOrWhiteSpace(minio.SecretKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Deal.Infrastructure.Integrations.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Расширения <see cref="string"/> для разбора конфигурационных значений.
|
||||
/// </summary>
|
||||
internal static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Разбирает строковое значение как булев флаг конфигурации: «true» (без учёта регистра) или «1».
|
||||
/// </summary>
|
||||
/// <param name="raw">Сырое значение настройки.</param>
|
||||
/// <returns>True — значение распознано как включённое.</returns>
|
||||
public static bool IsTrue(this string raw)
|
||||
{
|
||||
return string.Equals(raw, "true", StringComparison.OrdinalIgnoreCase) || raw == "1";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Deal.Infrastructure.Integrations;
|
||||
|
||||
/// <summary>
|
||||
/// Расширения <see cref="Uri"/> для SSRF-гейта интеграций (проверка приватности адреса).
|
||||
/// </summary>
|
||||
internal static class UriExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Проверяет, указывает ли URL на приватный/loopback/link-local адрес (SSRF-гейт).
|
||||
/// Распознаются IP-литералы (IPv4/IPv6) и имя localhost; DNS-имена считаются публичными
|
||||
/// (полный egress-контроль с резолвом выполняется на сетевом периметре).
|
||||
/// </summary>
|
||||
/// <param name="uri">Абсолютный http(s)-адрес.</param>
|
||||
/// <returns>True — адрес приватный/локальный (HTTP к нему запрещён).</returns>
|
||||
public static bool IsPrivateEndpoint(this Uri uri)
|
||||
{
|
||||
string host = uri.Host;
|
||||
if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IPAddress.TryParse(host, out IPAddress? address))
|
||||
{
|
||||
return false; // DNS-имя — резолв вне этого слоя
|
||||
}
|
||||
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
if (IPAddress.IsLoopback(address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (address.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
byte[] bytes = address.GetAddressBytes();
|
||||
return bytes[0] == 10
|
||||
|| (bytes[0] == 172 && bytes[1] is >= 16 and <= 31)
|
||||
|| (bytes[0] == 192 && bytes[1] == 168)
|
||||
|| bytes[0] == 169 && bytes[1] == 254 // link-local (включая 169.254.169.254 metadata)
|
||||
|| bytes[0] == 0;
|
||||
}
|
||||
|
||||
// IPv6: уникальные локальные (fc00::/7) и link-local (fe80::/10).
|
||||
byte[] v6 = address.GetAddressBytes();
|
||||
return (v6[0] & 0xFE) == 0xFC || (v6[0] == 0xFE && (v6[1] & 0xC0) == 0x80);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user