246 lines
8.2 KiB
C#
246 lines
8.2 KiB
C#
using BeWo.ServerUtils.Core;
|
|
using BS.Shared.Core;
|
|
using BS.Shared.DataContracts;
|
|
using BS.Shared.Exceptions;
|
|
using BS.Shared.Interface;
|
|
using log4net;
|
|
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
|
|
{
|
|
private ILog _logger;
|
|
|
|
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);
|
|
|
|
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;
|
|
if (!string.IsNullOrWhiteSpace(api_key))
|
|
SetToken(api_key);
|
|
DirectConnect = directConntect;
|
|
|
|
_logger = LoggerUtils.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
|
}
|
|
|
|
/// <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);
|
|
|
|
var json = await http_response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
|
|
if (!http_response.IsSuccessStatusCode)
|
|
{
|
|
_logger.Error($"{http_response.ReasonPhrase} (Code:{http_response.StatusCode})");
|
|
_logger.Error(json);
|
|
|
|
return ApiResponse<T>.FailureResponse(AppError.ApiResponseFailed, http_response.ReasonPhrase, (int)http_response.StatusCode);
|
|
}
|
|
|
|
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);
|
|
|
|
var content = await http_response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
|
|
if (!http_response.IsSuccessStatusCode)
|
|
{
|
|
_logger.Error($"{http_response.ReasonPhrase} (Code:{http_response.StatusCode})");
|
|
_logger.Error(content);
|
|
|
|
return ApiResponse<T>.FailureResponse(AppError.ApiResponseFailed, http_response.ReasonPhrase, (int)http_response.StatusCode);
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
}
|