Created
February 23, 2022 12:42
-
-
Save msx752/dae6978d783661df86d4f99356ecec5e to your computer and use it in GitHub Desktop.
Ocelot WebApplicationFactory
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Ocelot.Configuration; | |
using Ocelot.Logging; | |
using Ocelot.Requester; | |
using System; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Http; | |
public class CustomHttpClientBuilder : Ocelot.Requester.IHttpClientBuilder | |
{ | |
private readonly IHttpClientCache _cacheHandlers; | |
private readonly ICustomOcelotMessageHandler _customOcelotMessageHandler; | |
private readonly TimeSpan _defaultTimeout; | |
private readonly IDelegatingHandlerHandlerFactory _factory; | |
private readonly IOcelotLogger _logger; | |
private DownstreamRoute _cacheKey; | |
private IHttpClient _client; | |
private HttpClient _httpClient; | |
public CustomHttpClientBuilder( | |
IDelegatingHandlerHandlerFactory factory, | |
IHttpClientCache cacheHandlers, | |
IOcelotLogger logger, | |
ICustomOcelotMessageHandler customOcelotMessageHandler) | |
{ | |
_factory = factory; | |
_cacheHandlers = cacheHandlers; | |
_logger = logger; | |
// This is hardcoded at the moment but can easily be added to configuration | |
// if required by a user request. | |
_defaultTimeout = TimeSpan.FromSeconds(90); | |
_customOcelotMessageHandler = customOcelotMessageHandler; | |
} | |
public IHttpClient Create(DownstreamRoute downstreamRoute) | |
{ | |
_cacheKey = downstreamRoute; | |
var httpClient = _cacheHandlers.Get(_cacheKey); | |
if (httpClient != null) | |
{ | |
_client = httpClient; | |
return httpClient; | |
} | |
var handler = CreateHandler(downstreamRoute); | |
if (handler is HttpClientHandler && downstreamRoute.DangerousAcceptAnyServerCertificateValidator) | |
{ | |
((HttpClientHandler)handler).ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => true; | |
_logger | |
.LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {downstreamRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {downstreamRoute.DownstreamPathTemplate}"); | |
} | |
var timeout = downstreamRoute.QosOptions.TimeoutValue == 0 | |
? _defaultTimeout | |
: TimeSpan.FromMilliseconds(downstreamRoute.QosOptions.TimeoutValue); | |
_httpClient = new HttpClient(CreateHttpMessageHandler(handler, downstreamRoute)) | |
{ | |
Timeout = timeout | |
}; | |
_client = new HttpClientWrapper(_httpClient); | |
return _client; | |
} | |
public void Save() | |
{ | |
_cacheHandlers.Set(_cacheKey, _client, TimeSpan.FromHours(24)); | |
} | |
private HttpMessageHandler CreateHandler(DownstreamRoute downstreamRoute) | |
{ | |
var wafHandler = _customOcelotMessageHandler.Gethandler(downstreamRoute); | |
if (wafHandler != null) | |
return wafHandler; // waf routing, if not defined searches real ip/port below | |
// Dont' create the CookieContainer if UseCookies is not set or the HttpClient will complain | |
// under .Net Full Framework | |
var useCookies = downstreamRoute.HttpHandlerOptions.UseCookieContainer; | |
return useCookies ? UseCookiesHandler(downstreamRoute) : UseNonCookiesHandler(downstreamRoute); | |
} | |
private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler, DownstreamRoute request) | |
{ | |
//todo handle error | |
var handlers = _factory.Get(request).Data; | |
handlers | |
.Select(handler => handler) | |
.Reverse() | |
.ToList() | |
.ForEach(handler => | |
{ | |
var delegatingHandler = handler(); | |
delegatingHandler.InnerHandler = httpMessageHandler; | |
httpMessageHandler = delegatingHandler; | |
}); | |
return httpMessageHandler; | |
} | |
private HttpClientHandler UseCookiesHandler(DownstreamRoute downstreamRoute) | |
{ | |
return new HttpClientHandler | |
{ | |
AllowAutoRedirect = downstreamRoute.HttpHandlerOptions.AllowAutoRedirect, | |
UseCookies = downstreamRoute.HttpHandlerOptions.UseCookieContainer, | |
UseProxy = downstreamRoute.HttpHandlerOptions.UseProxy, | |
MaxConnectionsPerServer = downstreamRoute.HttpHandlerOptions.MaxConnectionsPerServer, | |
CookieContainer = new CookieContainer(), | |
}; | |
} | |
private HttpClientHandler UseNonCookiesHandler(DownstreamRoute downstreamRoute) | |
{ | |
return new HttpClientHandler | |
{ | |
AllowAutoRedirect = downstreamRoute.HttpHandlerOptions.AllowAutoRedirect, | |
UseCookies = downstreamRoute.HttpHandlerOptions.UseCookieContainer, | |
UseProxy = downstreamRoute.HttpHandlerOptions.UseProxy, | |
MaxConnectionsPerServer = downstreamRoute.HttpHandlerOptions.MaxConnectionsPerServer, | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.AspNetCore.Http; | |
using Ocelot.Logging; | |
using Ocelot.Middleware; | |
using Ocelot.Requester; | |
using Ocelot.Responses; | |
using System; | |
using System.Net.Http; | |
using System.Threading.Tasks; | |
public class CustomHttpClientHttpRequester : IHttpRequester | |
{ | |
private readonly IHttpClientCache _cacheHandlers; | |
private readonly ICustomOcelotMessageHandler _customOcelotMessageHandler; | |
private readonly IDelegatingHandlerHandlerFactory _factory; | |
private readonly IOcelotLogger _logger; | |
private readonly IExceptionToErrorMapper _mapper; | |
public CustomHttpClientHttpRequester(IOcelotLoggerFactory loggerFactory, | |
IHttpClientCache cacheHandlers, | |
IDelegatingHandlerHandlerFactory factory, | |
IExceptionToErrorMapper mapper, | |
ICustomOcelotMessageHandler customOcelotMessageHandler) | |
{ | |
_logger = loggerFactory.CreateLogger<CustomHttpClientHttpRequester>(); | |
_cacheHandlers = cacheHandlers; | |
_factory = factory; | |
_mapper = mapper; | |
_customOcelotMessageHandler = customOcelotMessageHandler; | |
} | |
public async Task<Response<HttpResponseMessage>> GetResponse(HttpContext httpContext) | |
{ | |
var builder = new CustomHttpClientBuilder(_factory, _cacheHandlers, _logger, _customOcelotMessageHandler); | |
var downstreamRoute = httpContext.Items.DownstreamRoute(); | |
var downstreamRequest = httpContext.Items.DownstreamRequest(); | |
var httpClient = builder.Create(downstreamRoute); | |
try | |
{ | |
var response = await httpClient.SendAsync(downstreamRequest.ToHttpRequestMessage(), httpContext.RequestAborted); | |
return new OkResponse<HttpResponseMessage>(response); | |
} | |
catch (Exception exception) | |
{ | |
var error = _mapper.Map(exception); | |
return new ErrorResponse<HttpResponseMessage>(error); | |
} | |
finally | |
{ | |
builder.Save(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Ocelot.Configuration; | |
using System; | |
using System.Collections.Generic; | |
using System.Net.Http; | |
using System.Reflection; | |
public class CustomOcelotMessageHandler : ICustomOcelotMessageHandler | |
{ | |
private Dictionary<string, HttpMessageHandler> handlers = new Dictionary<string, HttpMessageHandler>(); | |
public CustomOcelotMessageHandler(HttpClient[] httpClients) | |
{ | |
foreach (var item in httpClients) | |
{ | |
var kv = GetHttpClientHandler(item); | |
handlers.Add(kv.Key, kv.Value); | |
} | |
} | |
public HttpMessageHandler Gethandler(DownstreamRoute downstreamRoute) | |
{ | |
foreach (var downstreamHostAndPort in downstreamRoute.DownstreamAddresses) | |
{ | |
if (handlers.TryGetValue($"{downstreamHostAndPort.Host.ToLowerInvariant()}:{downstreamHostAndPort.Port}", out HttpMessageHandler handler)) | |
return handler; | |
} | |
return null; | |
} | |
private static KeyValuePair<string, HttpMessageHandler> GetHttpClientHandler(HttpClient httpClient) | |
{ | |
var handlerField = httpClient | |
.GetType().BaseType | |
.GetField("_handler", BindingFlags.NonPublic | BindingFlags.Instance); | |
var handler = (HttpMessageHandler)handlerField.GetValue(httpClient); | |
var handlerType = handler.GetType(); | |
if (!handlerType.FullName.StartsWith("Microsoft.AspNetCore.Mvc.Testing")) | |
throw new ArgumentException($"selected HttpClient doesn't belongs to WebApplicationFactory, this is only for Testing purpose."); | |
return new KeyValuePair<string, HttpMessageHandler>($"{httpClient.BaseAddress.Host.ToLowerInvariant()}:{httpClient.BaseAddress.Port}", handler); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Ocelot.Configuration; | |
using System.Net.Http; | |
public interface ICustomOcelotMessageHandler | |
{ | |
HttpMessageHandler Gethandler(DownstreamRoute downstreamRoute); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.AspNetCore.Mvc.Testing; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.DependencyInjection.Extensions; | |
using Ocelot.Requester; | |
using System; | |
using System.Net.Http; | |
public static class OcelotRedirectExtensions | |
{ | |
public static void AddOcelotWAFRedirections(this IServiceCollection services, HttpClient[] httpClients) | |
{ | |
if (httpClients == null) | |
return; | |
var _ocelotMessageHandlers = new CustomOcelotMessageHandler(httpClients); | |
services.AddSingleton(typeof(ICustomOcelotMessageHandler), x => _ocelotMessageHandlers); | |
var descriptorIHttpRequester = new ServiceDescriptor(typeof(IHttpRequester), typeof(CustomHttpClientHttpRequester), | |
ServiceLifetime.Singleton); | |
services.Replace(descriptorIHttpRequester); | |
} | |
/// <summary> | |
/// creates virtual HttpContext for the WebApplicationFactory for given url format. | |
/// </summary> | |
/// <typeparam name="TStartup"></typeparam> | |
/// <param name="webApplicationFactory"></param> | |
/// <param name="port">virtual port default: 80</param> | |
/// <param name="host">virtual hostname default: localhost</param> | |
/// <param name="scheme">virtual scheme default: http</param> | |
/// <returns></returns> | |
/// <exception cref="ArgumentException"></exception> | |
public static HttpClient CreateOcelotClient<TStartup>(this WebApplicationFactory<TStartup> webApplicationFactory, | |
int port, | |
string host = "localhost") | |
where TStartup : class | |
{ | |
if (Uri.TryCreate(new Uri($"http://{host}:{port}"), "", out Uri generatedVirtualuri)) | |
{ | |
return webApplicationFactory.CreateClient(new WebApplicationFactoryClientOptions() | |
{ | |
BaseAddress = generatedVirtualuri | |
}); | |
} | |
throw new ArgumentException($"Invalid Uri Format: '{$"http://{host}:{port}"}' please use valid Uri format."); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Mvc.Testing; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.Hosting; | |
using System.Net.Http; | |
public class OcelotWebApplicationFactory : WebApplicationFactory<WAFOcelot.Startup> | |
{ | |
private readonly HttpClient[] httpClients; | |
public OcelotWebApplicationFactory(HttpClient[] httpClients) | |
{ | |
this.httpClients = httpClients; | |
} | |
public OcelotWebApplicationFactory() | |
{ | |
} | |
protected override IHostBuilder CreateHostBuilder() | |
{ | |
return Host.CreateDefaultBuilder(new string[0]).ConfigureWebHostDefaults((wb) => | |
{ | |
wb.UseEnvironment("Development"); | |
wb.ConfigureAppConfiguration((hostingContext, config) => | |
{ | |
config.AddJsonFile("ocelot.json", false, true); | |
}); | |
wb.UseStartup<WAFOcelot.Startup>(); | |
wb.ConfigureServices(services => | |
{ | |
services.AddOcelotWAFRedirections(httpClients); | |
}); | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment