diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index ad8f19b5a..22a0019a1 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -40,6 +40,7 @@ public class DownstreamRouteBuilder private Dictionary _upstreamHeaders; private MetadataOptions _metadataOptions; private int? _timeout; + private bool _connectionClose; public DownstreamRouteBuilder() { @@ -253,6 +254,12 @@ public DownstreamRouteBuilder WithTimeout(int? timeout) return this; } + public DownstreamRouteBuilder WithConnectionClose(bool connectionClose) + { + _connectionClose = connectionClose; + return this; + } + public DownstreamRoute Build() { return new DownstreamRoute( @@ -288,6 +295,7 @@ public DownstreamRoute Build() _downstreamHttpVersionPolicy, _upstreamHeaders, _metadataOptions, - _timeout); + _timeout, + _connectionClose); } } diff --git a/src/Ocelot/Configuration/Creator/ConnectionCloseCreator.cs b/src/Ocelot/Configuration/Creator/ConnectionCloseCreator.cs new file mode 100644 index 000000000..fd0d330cd --- /dev/null +++ b/src/Ocelot/Configuration/Creator/ConnectionCloseCreator.cs @@ -0,0 +1,14 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public class ConnectionCloseCreator : IConnectionCloseCreator + { + public bool Create(bool fileRouteConnectionClose, FileGlobalConfiguration globalConfiguration) + { + var globalConnectionClose = globalConfiguration.ConnectionClose; + + return fileRouteConnectionClose || globalConnectionClose; + } + } +} diff --git a/src/Ocelot/Configuration/Creator/IConnectionCloseCreator.cs b/src/Ocelot/Configuration/Creator/IConnectionCloseCreator.cs new file mode 100644 index 000000000..8fea30c92 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IConnectionCloseCreator.cs @@ -0,0 +1,9 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public interface IConnectionCloseCreator + { + bool Create(bool fileRouteConnectionClose, FileGlobalConfiguration globalConfiguration); + } +} diff --git a/src/Ocelot/Configuration/DownstreamRoute.cs b/src/Ocelot/Configuration/DownstreamRoute.cs index 42ddcd037..3cebd60dd 100644 --- a/src/Ocelot/Configuration/DownstreamRoute.cs +++ b/src/Ocelot/Configuration/DownstreamRoute.cs @@ -39,7 +39,8 @@ public DownstreamRoute( HttpVersionPolicy downstreamHttpVersionPolicy, Dictionary upstreamHeaders, MetadataOptions metadataOptions, - int? timeout) + int? timeout, + bool connectionClose) { DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; AddHeadersToDownstream = addHeadersToDownstream; @@ -74,6 +75,7 @@ public DownstreamRoute( UpstreamHeaders = upstreamHeaders ?? new(); MetadataOptions = metadataOptions; Timeout = timeout; + ConnectionClose = connectionClose; } public string Key { get; } @@ -84,6 +86,7 @@ public DownstreamRoute( public string ServiceName { get; } public string ServiceNamespace { get; } public HttpHandlerOptions HttpHandlerOptions { get; } + public bool ConnectionClose { get; } public QoSOptions QosOptions { get; } public string DownstreamScheme { get; } public string RequestIdKey { get; } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 6a47c0fe0..be7514dfb 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -7,6 +7,7 @@ public FileGlobalConfiguration() AuthenticationOptions = new(); BaseUrl = default; CacheOptions = default; + ConnectionClose = false; DownstreamHeaderTransform = new Dictionary(); DownstreamHttpVersion = default; DownstreamHttpVersionPolicy = default; @@ -27,6 +28,7 @@ public FileGlobalConfiguration() public FileGlobalAuthenticationOptions AuthenticationOptions { get; set; } public string BaseUrl { get; set; } public FileGlobalCacheOptions CacheOptions { get; set; } + public bool ConnectionClose { get; set; } public IDictionary DownstreamHeaderTransform { get; set; } public string DownstreamHttpVersion { get; set; } public string DownstreamHttpVersionPolicy { get; set; } diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index 796e527a3..84e1f2065 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -4,7 +4,7 @@ /// Represents the JSON structure of a standard static route (no service discovery). /// public class FileRoute : FileRouteBase, IRouteUpstream, IRouteGrouping, IRouteRateLimiting, ICloneable -{ +{ public FileRoute() { AddClaimsToRequest = new Dictionary(); @@ -20,6 +20,7 @@ public FileRoute() UpstreamHeaderTemplates = new Dictionary(); UpstreamHeaderTransform = new Dictionary(); UpstreamHttpMethod = new(); + ConnectionClose = false; } public FileRoute(FileRoute from) @@ -31,6 +32,7 @@ public FileRoute(FileRoute from) public Dictionary AddHeadersToRequest { get; set; } public Dictionary AddQueriesToRequest { get; set; } public Dictionary ChangeDownstreamPathTemplate { get; set; } + public bool ConnectionClose { get; set; } public bool DangerousAcceptAnyServerCertificateValidator { get; set; } public List DelegatingHandlers { get; set; } public IDictionary DownstreamHeaderTransform { get; set; } @@ -68,6 +70,7 @@ public static void DeepCopy(FileRoute from, FileRoute to) to.AddQueriesToRequest = new(from.AddQueriesToRequest); to.AuthenticationOptions = from.AuthenticationOptions is null ? null : new(from.AuthenticationOptions); to.ChangeDownstreamPathTemplate = new(from.ChangeDownstreamPathTemplate); + to.ConnectionClose = from.ConnectionClose; to.DangerousAcceptAnyServerCertificateValidator = from.DangerousAcceptAnyServerCertificateValidator; to.DelegatingHandlers = new(from.DelegatingHandlers); to.DownstreamHeaderTransform = new Dictionary(from.DownstreamHeaderTransform); @@ -114,5 +117,5 @@ public override string ToString() return !string.IsNullOrWhiteSpace(ServiceName) ? string.Join(':', ServiceNamespace, ServiceName, path) : path; - } + } } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index ff343fafe..09d859bf6 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -125,6 +125,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); + Services.TryAddSingleton(); // Add security Services.TryAddSingleton(); diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs new file mode 100644 index 000000000..6cef70d42 --- /dev/null +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -0,0 +1,128 @@ +using Ocelot.Configuration; +using Ocelot.Logging; + +namespace Ocelot.Requester; + +public interface IHttpClientBuilder { } +public interface IHttpClientCache +{ + IHttpClient Get(DownstreamRoute cacheKey); + void Set(DownstreamRoute cacheKey, IHttpClient client, TimeSpan span); +} + +public class HttpClientBuilder : IHttpClientBuilder +{ + private readonly IDelegatingHandlerFactory _factory; + private readonly IHttpClientCache _cacheHandlers; + private readonly IOcelotLogger _logger; + private DownstreamRoute _cacheKey; + private HttpClient _httpClient; + private IHttpClient _client; + private readonly TimeSpan _defaultTimeout; + + public HttpClientBuilder( + IDelegatingHandlerFactory factory, + IHttpClientCache cacheHandlers, + IOcelotLogger logger) + { + _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); + } + + public IHttpClient Create(DownstreamRoute downstreamRoute) + { + _cacheKey = downstreamRoute; + + var httpClient = _cacheHandlers.Get(_cacheKey); + + if (httpClient != null) + { + _client = httpClient; + return httpClient; + } + + var handler = CreateHandler(downstreamRoute); + + if (downstreamRoute.DangerousAcceptAnyServerCertificateValidator) + { + handler.ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + + _logger + .LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {downstreamRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {downstreamRoute.DownstreamPathTemplate}"); + } + + var timeout = downstreamRoute.QosOptions.Timeout == 0 + ? _defaultTimeout + : TimeSpan.FromMilliseconds(downstreamRoute.QosOptions.Timeout.Value); + + _httpClient = new HttpClient(CreateHttpMessageHandler(handler, downstreamRoute)) + { + Timeout = timeout, + }; + + _client = new HttpClientWrapper(_httpClient, downstreamRoute.ConnectionClose); // TODO + + return _client; + } + + private static HttpClientHandler CreateHandler(DownstreamRoute downstreamRoute) + { + // 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 static HttpClientHandler UseNonCookiesHandler(DownstreamRoute downstreamRoute) + { + return new HttpClientHandler + { + AllowAutoRedirect = downstreamRoute.HttpHandlerOptions.AllowAutoRedirect, + UseCookies = downstreamRoute.HttpHandlerOptions.UseCookieContainer, + UseProxy = downstreamRoute.HttpHandlerOptions.UseProxy, + MaxConnectionsPerServer = downstreamRoute.HttpHandlerOptions.MaxConnectionsPerServer, + + }; + } + + private static 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(), + }; + } + + public void Save() + { + _cacheHandlers.Set(_cacheKey, _client, TimeSpan.FromHours(24)); + } + + private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler, DownstreamRoute request) + { + //todo handle error + var handlers = _factory.Get(request); + + handlers + .Select(handler => handler) + .Reverse() + .ToList() + .ForEach(handler => + { + handler.InnerHandler = httpMessageHandler; + httpMessageHandler = handler; + }); + return httpMessageHandler; + } +} diff --git a/src/Ocelot/Requester/HttpClientWrapper.cs b/src/Ocelot/Requester/HttpClientWrapper.cs new file mode 100644 index 000000000..8c9ac82d5 --- /dev/null +++ b/src/Ocelot/Requester/HttpClientWrapper.cs @@ -0,0 +1,30 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Ocelot.Requester +{ + public interface IHttpClient { } + + /// + /// This class was made to make unit testing easier when HttpClient is used. + /// + public class HttpClientWrapper : IHttpClient + { + public HttpClient Client { get; } + + public bool ConnectionClose { get; } // TODO + + public HttpClientWrapper(HttpClient client, bool connectionClose = false) // TODO + { + Client = client; + ConnectionClose = connectionClose; + } + + public Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + request.Headers.ConnectionClose = ConnectionClose; // TODO + return Client.SendAsync(request, cancellationToken); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs index 829ed32b0..6322170d1 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -47,7 +47,8 @@ public DownstreamRouteExtensionsTests() HttpVersionPolicy.RequestVersionExact, new(), new MetadataOptions(new FileMetadataOptions()), - 0); + 0, + false); } [Theory] diff --git a/test/Ocelot.UnitTests/Requester/MessageInvokerCacheKeyTests.cs b/test/Ocelot.UnitTests/Requester/MessageInvokerCacheKeyTests.cs index f26fe0dc9..e20791438 100644 --- a/test/Ocelot.UnitTests/Requester/MessageInvokerCacheKeyTests.cs +++ b/test/Ocelot.UnitTests/Requester/MessageInvokerCacheKeyTests.cs @@ -64,8 +64,8 @@ static bool EqualsForGui(DownstreamRoute x, DownstreamRoute y) } // Two object with absolutely identical internal state - var d1 = new DownstreamRoute(default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default); - var d2 = new DownstreamRoute(default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default); + var d1 = new DownstreamRoute(default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default); + var d2 = new DownstreamRoute(default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default); // Act for Gui :) This will be a gift for Gui for Christmas! LOL bool happyStart = d1.Equals(d2); // object.Equals(object)