Files
BeWoPlaner/Server/Components/ServerUtils/ApiFacade/HttpClientFacade.cs
2025-11-28 14:54:24 +01:00

232 lines
7.7 KiB
C#

using BS.Shared.Core;
using BS.Shared.DataContracts;
using BS.Shared.Exceptions;
using BS.Shared.Interface;
using System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace BeWo.ServerUtils.ApiFacade
{
public class HttpClientFacade
{
public HttpClientFacade(string base_url, string api_key, bool directConntect = true)
{
if (string.IsNullOrEmpty(base_url))
throw new ArgumentNullException("HttpClient: base_url is missing");
if (string.IsNullOrWhiteSpace(api_key))
throw new ArgumentNullException("HttpClient: token is missing");
Base_Url = base_url;
SetToken(api_key);
DirectConnect = directConntect;
}
public bool DirectConnect { get; private set; }
public string Base_Url { get; private set; }
public HttpMethod Method { get; set; } = HttpMethod.Get;
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
public Dictionary<string, string> Parameters { get; set; } = new Dictionary<string, string>();
public string ContentType { get; set; } = "application/x-www-form-urlencoded";
public Encoding Encoding { get; set; } = Encoding.UTF8;
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Führt einen Request aus, erwartet einen komplexen Datentyp: Schaue Verweise für Beispiele.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="method"></param>
/// <param name="anonymousType"></param>
/// <param name="extractor"></param>
/// <returns></returns>
public virtual async Task<ApiResponse<T>> GetAnonymousTypeAsync<T>(string method, T anonymousType, IApiErrorExtractor extractor)
{
var http_response = await getResponseAsync(method).ConfigureAwait(false);
if (!http_response.IsSuccessStatusCode)
{
return ApiResponse<T>.FailureResponse(AppError.ApiResponseFailed, http_response.ReasonPhrase, (int)http_response.StatusCode);
}
var json = await http_response.Content.ReadAsStringAsync().ConfigureAwait(false);
return parse(() => JsonSerializer.Deserialize<T>(json, JsonSerializerOptions.Default), json, extractor);
}
/// <summary>
/// Führt einen Request aus, erwartet einen einfachen Datentyp wie: string, int, decimal, byte[] -
/// Liste kann gerne erweitert werden
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="method"></param>
/// <param name="extractor"></param>
/// <returns></returns>
/// <exception cref="BeWoInvalidOperationException"></exception>
public virtual async Task<ApiResponse<T>> GetAsync<T>(string method, IApiErrorExtractor extractor)
{
var http_response = await getResponseAsync(method).ConfigureAwait(false);
if (!http_response.IsSuccessStatusCode)
{
return ApiResponse<T>.FailureResponse(AppError.ApiResponseFailed, http_response.ReasonPhrase, (int)http_response.StatusCode);
}
var content = await http_response.Content.ReadAsStringAsync().ConfigureAwait(false);
object result;
var type = typeof(T);
if (type == typeof(string))
{
result = content;
}
else if (type == typeof(int))
{
result = int.Parse(content);
}
else if (type == typeof(decimal))
{
result = decimal.Parse(content);
}
else if (type == typeof(byte[]))
{
result = await http_response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
else
{
throw new BeWoInvalidOperationException(AppError.UnsupportedType(typeof(T)));
}
return parse(() => (T)result, content, extractor);
}
private static bool ServerCertificateCustomValidation(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslErrors)
{
// It is possible to inspect the certificate provided by the server.
Console.WriteLine($"Requested URI: {requestMessage.RequestUri}");
Console.WriteLine($"Effective date: {certificate?.GetEffectiveDateString()}");
Console.WriteLine($"Exp date: {certificate?.GetExpirationDateString()}");
Console.WriteLine($"Issuer: {certificate?.Issuer}");
Console.WriteLine($"Subject: {certificate?.Subject}");
// Based on the custom logic it is possible to decide whether the client considers certificate valid or not
Console.WriteLine($"Errors: {sslErrors}");
return sslErrors == SslPolicyErrors.None;
}
private protected virtual async Task<HttpResponseMessage> getResponseAsync(string method)
{
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation,
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls,
};
using (var client = new HttpClient(handler) { Timeout = Timeout })
{
var requestUri = BS.Shared.Core.Utilities.WebUtils.CombineUrl(Base_Url, method);
if (Method == HttpMethod.Get && Parameters.Count > 0)
{
var query = string.Join("&", Parameters.Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));
requestUri += (Base_Url.Contains("?") ? "&" : "?") + query;
}
using (var request = new HttpRequestMessage(Method, requestUri))
{
await checkForDirect(request).ConfigureAwait(false);
// Add headers
foreach (var header in Headers)
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
// Add body if POST or PUT
if (Method == HttpMethod.Post || Method == HttpMethod.Put)
{
var content = new FormUrlEncodedContent(Parameters);
var bytes = await content.ReadAsByteArrayAsync().ConfigureAwait(false);
request.Content = new ByteArrayContent(bytes);
request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(ContentType);
}
return await client.SendAsync(request).ConfigureAwait(false);
}
}
}
private protected async Task checkForDirect(HttpRequestMessage request)
{
if (!DirectConnect || request.RequestUri is null)
return;
var host = request.RequestUri.DnsSafeHost;
var isSslSession = request.RequestUri.ToString().StartsWith("https://");
try
{
// DNS-Auflösung für den Hostnamen durchführen
var ipAddresses = await Dns.GetHostAddressesAsync(host).ConfigureAwait(false);
// Erste verfügbare IP-Adresse verwenden (IPv4 bevorzugen)
var targetIp = ipAddresses.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork)
?? ipAddresses.FirstOrDefault();
if (targetIp != null)
{
request.RequestUri = new Uri($"{(isSslSession ? "https://" : "http://")}{targetIp}{request.RequestUri.PathAndQuery}");
request.Headers.Host = host;
}
}
catch (Exception ex)
{
// Fallback: Original URI beibehalten bei DNS-Fehlern
// Optional: Logging des Fehlers
Console.WriteLine($"DNS resolution failed for {host}: {ex.Message}");
}
}
private void SetToken(string api_key)
{
var key = nameof(HttpRequestHeader.Authorization);
var token = $"Bearer {api_key}";
if (Headers.ContainsKey(key))
{
Headers[key] = token;
}
else
{
Headers.Add(key, token);
}
}
private ApiResponse<T> parse<T>(Func<T> func, string json, IApiErrorExtractor errorExtractor)
{
try
{
// Versuch, die erwartete Struktur zu deserialisieren
var data = func();
return ApiResponse<T>.SuccessResponse(data);
}
catch (JsonException)
{
// Falls normale Deserialisierung fehlschlägt, versuche Fehler zu extrahieren
return ApiResponse<T>.FailureResponse(AppError.JsonParsingError, errorExtractor.ExtractError(json).ToList());
}
}
}
}