diff --git a/.gitignore b/.gitignore index 9340cd0dc..0f60ca3d5 100644 --- a/.gitignore +++ b/.gitignore @@ -248,4 +248,7 @@ test/Ocelot.AcceptanceTests/configuration.json # Read the docstates _build/ _static/ -_templates/ \ No newline at end of file +_templates/ + +# JetBrains Rider +.idea/ \ No newline at end of file diff --git a/docs/features/headerstransformation.rst b/docs/features/headerstransformation.rst new file mode 100644 index 000000000..e6785cec9 --- /dev/null +++ b/docs/features/headerstransformation.rst @@ -0,0 +1,70 @@ +Headers Transformation +===================== + +Ocelot allows the user to transform headers pre and post downstream request. At the moment Ocelot only supports find and replace. This feature was requested `GitHub #190 `_ and I decided that it was going to be useful in various ways. + +Syntax +^^^^^^ + +In order to transform a header first we specify the header key and then the type of transform we want e.g. + +.. code-block:: json + + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + +The key is "Test" and the value is "http://www.bbc.co.uk/, http://ocelot.com/". The value is saying replace http://www.bbc.co.uk/ with http://ocelot.com/. The syntax is {find}, {replace}. Hopefully pretty simple. There are examples below that explain more. + +Pre Downstream Request +^^^^^^^^^^^^^^^^^^^^^^ + +Add the following to a ReRoute in configuration.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This header will be changed before the request downstream and will be sent to the downstream server. + +.. code-block:: json + + "UpstreamHeaderTransform": { + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + +Post Downstream Request +^^^^^^^^^^^^^^^^^^^^^^ + +Add the following to a ReRoute in configuration.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This transformation will take place after Ocelot has received the response from the downstream service. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + +Placeholders +^^^^^^^^^^^^ + +Ocelot allows placeholders that can be used in header transformation. At the moment there is only one placeholder. + +{BaseUrl} - This will use Ocelot's base url e.g. http://localhost:5000 as its value. + +Handling 302 Redirects +^^^^^^^^^^^^^^^^^^^^^^ +Ocelot will by default automatically follow redirects however if you want to return the location header to the client you might want to change the location to be Ocelot not the downstream service. Ocelot allows this with the following configuration. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +or you could use the BaseUrl placeholder. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "http://localhost:6773, {BaseUrl}" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +Ocelot will not try and replace the location header returned by the downstream service with its own URL. diff --git a/docs/index.rst b/docs/index.rst index 0a292bcf7..dbd7c93d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,6 +27,7 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n features/raft features/caching features/qualityofservice + features/headerstransformation features/claimstransformation features/logging features/requestid diff --git a/run-acceptance-tests.sh b/run-acceptance-tests.sh new file mode 100755 index 000000000..e05baea12 --- /dev/null +++ b/run-acceptance-tests.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +./build.sh --target RunAcceptanceTests \ No newline at end of file diff --git a/run-unit-tests.sh b/run-unit-tests.sh new file mode 100755 index 000000000..da848514e --- /dev/null +++ b/run-unit-tests.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +./build.sh --target RunUnitTests \ No newline at end of file diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index cc5a61aa1..d81d3351b 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -36,6 +36,9 @@ public class ReRouteBuilder private bool _useServiceDiscovery; private string _serviceName; + private List _upstreamHeaderFindAndReplace; + private List _downstreamHeaderFindAndReplace; + public ReRouteBuilder WithLoadBalancer(string loadBalancer) { _loadBalancer = loadBalancer; @@ -198,6 +201,18 @@ public ReRouteBuilder WithServiceName(string serviceName) return this; } + public ReRouteBuilder WithUpstreamHeaderFindAndReplace(List upstreamHeaderFindAndReplace) + { + _upstreamHeaderFindAndReplace = upstreamHeaderFindAndReplace; + return this; + } + + public ReRouteBuilder WithDownstreamHeaderFindAndReplace(List downstreamHeaderFindAndReplace) + { + _downstreamHeaderFindAndReplace = downstreamHeaderFindAndReplace; + return this; + } + public ReRoute Build() { return new ReRoute( @@ -226,7 +241,9 @@ public ReRoute Build() _rateLimitOptions, _httpHandlerOptions, _useServiceDiscovery, - _serviceName); + _serviceName, + _upstreamHeaderFindAndReplace, + _downstreamHeaderFindAndReplace); } } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index a701d9a05..32c5fb47b 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -37,6 +37,7 @@ public class FileOcelotConfigurationCreator : IOcelotConfigurationCreator private readonly IRegionCreator _regionCreator; private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private readonly IAdministrationPath _adminPath; + private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; public FileOcelotConfigurationCreator( @@ -53,9 +54,11 @@ public FileOcelotConfigurationCreator( IRateLimitOptionsCreator rateLimitOptionsCreator, IRegionCreator regionCreator, IHttpHandlerOptionsCreator httpHandlerOptionsCreator, - IAdministrationPath adminPath + IAdministrationPath adminPath, + IHeaderFindAndReplaceCreator headerFAndRCreator ) { + _headerFAndRCreator = headerFAndRCreator; _adminPath = adminPath; _regionCreator = regionCreator; _rateLimitOptionsCreator = rateLimitOptionsCreator; @@ -128,6 +131,8 @@ private ReRoute SetUpReRoute(FileReRoute fileReRoute, FileGlobalConfiguration gl var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileReRoute); + var hAndRs = _headerFAndRCreator.Create(fileReRoute); + var reRoute = new ReRouteBuilder() .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) @@ -155,6 +160,8 @@ private ReRoute SetUpReRoute(FileReRoute fileReRoute, FileGlobalConfiguration gl .WithHttpHandlerOptions(httpHandlerOptions) .WithServiceName(fileReRoute.ServiceName) .WithUseServiceDiscovery(fileReRoute.UseServiceDiscovery) + .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream) + .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) .Build(); return reRoute; diff --git a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs new file mode 100644 index 000000000..e7b613835 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using Ocelot.Configuration.File; +using Ocelot.Middleware; + +namespace Ocelot.Configuration.Creator +{ + public class HeaderFindAndReplaceCreator : IHeaderFindAndReplaceCreator + { + private IBaseUrlFinder _finder; + private Dictionary> _placeholders; + + public HeaderFindAndReplaceCreator(IBaseUrlFinder finder) + { + _finder = finder; + _placeholders = new Dictionary>(); + _placeholders.Add("{BaseUrl}", () => { + return _finder.Find(); + }); + } + + public HeaderTransformations Create(FileReRoute fileReRoute) + { + var upstream = new List(); + + foreach(var input in fileReRoute.UpstreamHeaderTransform) + { + var hAndr = Map(input); + upstream.Add(hAndr); + } + + var downstream = new List(); + + foreach(var input in fileReRoute.DownstreamHeaderTransform) + { + var hAndr = Map(input); + downstream.Add(hAndr); + } + + return new HeaderTransformations(upstream, downstream); + } + + private HeaderFindAndReplace Map(KeyValuePair input) + { + var findAndReplace = input.Value.Split(","); + + var replace = findAndReplace[1].TrimStart(); + + var startOfPlaceholder = replace.IndexOf("{"); + if(startOfPlaceholder > -1) + { + var endOfPlaceholder = replace.IndexOf("}", startOfPlaceholder); + + var placeholder = replace.Substring(startOfPlaceholder, startOfPlaceholder + (endOfPlaceholder + 1)); + + if(_placeholders.ContainsKey(placeholder)) + { + var value = _placeholders[placeholder].Invoke(); + replace = replace.Replace(placeholder, value); + } + } + + var hAndr = new HeaderFindAndReplace(input.Key, findAndReplace[0], replace, 0); + + return hAndr; + } + } +} diff --git a/src/Ocelot/Configuration/Creator/HeaderTransformations.cs b/src/Ocelot/Configuration/Creator/HeaderTransformations.cs new file mode 100644 index 000000000..2fba1c676 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/HeaderTransformations.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration.Creator +{ + public class HeaderTransformations + { + public HeaderTransformations(List upstream, List downstream) + { + Upstream = upstream; + Downstream = downstream; + } + + public List Upstream {get;private set;} + + public List Downstream {get;private set;} + } +} diff --git a/src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs new file mode 100644 index 000000000..1423c8cab --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public interface IHeaderFindAndReplaceCreator + { + HeaderTransformations Create(FileReRoute fileReRoute); + } +} diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index d33ba8e43..4a34878e4 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -11,17 +11,21 @@ public FileReRoute() AddClaimsToRequest = new Dictionary(); RouteClaimsRequirement = new Dictionary(); AddQueriesToRequest = new Dictionary(); + DownstreamHeaderTransform = new Dictionary(); FileCacheOptions = new FileCacheOptions(); QoSOptions = new FileQoSOptions(); RateLimitOptions = new FileRateLimitRule(); AuthenticationOptions = new FileAuthenticationOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); + UpstreamHeaderTransform = new Dictionary(); } public string DownstreamPathTemplate { get; set; } public string UpstreamPathTemplate { get; set; } public List UpstreamHttpMethod { get; set; } public Dictionary AddHeadersToRequest { get; set; } + public Dictionary UpstreamHeaderTransform { get; set; } + public Dictionary DownstreamHeaderTransform { get; set; } public Dictionary AddClaimsToRequest { get; set; } public Dictionary RouteClaimsRequirement { get; set; } public Dictionary AddQueriesToRequest { get; set; } diff --git a/src/Ocelot/Configuration/HeaderFindAndReplace.cs b/src/Ocelot/Configuration/HeaderFindAndReplace.cs new file mode 100644 index 000000000..f3835415c --- /dev/null +++ b/src/Ocelot/Configuration/HeaderFindAndReplace.cs @@ -0,0 +1,20 @@ +namespace Ocelot.Configuration +{ + public class HeaderFindAndReplace + { + public HeaderFindAndReplace(string key, string find, string replace, int index) + { + Key = key; + Find = find; + Replace = replace; + Index = index; + } + + public string Key {get;} + public string Find {get;} + public string Replace {get;} + + // only index 0 for now.. + public int Index {get;} + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index 18e068aaf..9efe6dbc6 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -32,8 +32,12 @@ public ReRoute(PathTemplate downstreamPathTemplate, RateLimitOptions ratelimitOptions, HttpHandlerOptions httpHandlerOptions, bool useServiceDiscovery, - string serviceName) + string serviceName, + List upstreamHeadersFindAndReplace, + List downstreamHeadersFindAndReplace) { + DownstreamHeadersFindAndReplace = downstreamHeadersFindAndReplace; + UpstreamHeadersFindAndReplace = upstreamHeadersFindAndReplace; ServiceName = serviceName; UseServiceDiscovery = useServiceDiscovery; ReRouteKey = reRouteKey; @@ -91,5 +95,8 @@ public ReRoute(PathTemplate downstreamPathTemplate, public HttpHandlerOptions HttpHandlerOptions { get; private set; } public bool UseServiceDiscovery {get;private set;} public string ServiceName {get;private set;} + public List UpstreamHeadersFindAndReplace {get;private set;} + public List DownstreamHeadersFindAndReplace {get;private set;} + } } \ No newline at end of file diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 3ddaf7d32..3b0a008f0 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -75,6 +75,9 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo //add ocelot services... _services.Configure(configurationRoot); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); diff --git a/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs b/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs index 7a4a66ea7..3a3b3b2dd 100644 --- a/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs +++ b/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs @@ -13,5 +13,6 @@ public DownstreamRoute(List templatePlaceholderNameAndV } public List TemplatePlaceholderNameAndValues { get; private set; } public ReRoute ReRoute { get; private set; } + public object UpstreamHeadersFindAndReplace {get;private set;} } } \ No newline at end of file diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index 96f2ca454..c2574ff95 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -37,7 +37,7 @@ public ExceptionHandlerMiddleware(RequestDelegate next, public async Task Invoke(HttpContext context) { try - { + { await TrySetGlobalRequestId(context); _logger.LogDebug("ocelot pipeline started"); @@ -85,7 +85,10 @@ private async Task TrySetGlobalRequestId(HttpContext context) private void SetInternalServerErrorOnResponse(HttpContext context) { - context.Response.StatusCode = 500; + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 500; + } } private string CreateMessage(HttpContext context, Exception e) diff --git a/src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs b/src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs new file mode 100644 index 000000000..83be9fe75 --- /dev/null +++ b/src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public class HttpContextRequestHeaderReplacer : IHttpContextRequestHeaderReplacer + { + public Response Replace(HttpContext context, List fAndRs) + { + foreach (var f in fAndRs) + { + if(context.Request.Headers.TryGetValue(f.Key, out var values)) + { + var replaced = values[f.Index].Replace(f.Find, f.Replace); + context.Request.Headers.Remove(f.Key); + context.Request.Headers.Add(f.Key, replaced); + } + } + + return new OkResponse(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs new file mode 100644 index 000000000..acd9af306 --- /dev/null +++ b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer + { + public Response Replace(HttpResponseMessage response, List fAndRs) + { + foreach (var f in fAndRs) + { + if(response.Headers.TryGetValues(f.Key, out var values)) + { + var replaced = values.ToList()[f.Index].Replace(f.Find, f.Replace); + response.Headers.Remove(f.Key); + response.Headers.Add(f.Key, replaced); + } + } + + return new OkResponse(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs b/src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs new file mode 100644 index 000000000..f0e969ec9 --- /dev/null +++ b/src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public interface IHttpContextRequestHeaderReplacer + { + Response Replace(HttpContext context, List fAndRs); + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs new file mode 100644 index 000000000..e3056ca66 --- /dev/null +++ b/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public interface IHttpResponseHeaderReplacer + { + Response Replace(HttpResponseMessage response, List fAndRs); + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs new file mode 100644 index 000000000..302efd18d --- /dev/null +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Headers.Middleware +{ + public class HttpHeadersTransformationMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IOcelotLogger _logger; + private readonly IHttpContextRequestHeaderReplacer _preReplacer; + private readonly IHttpResponseHeaderReplacer _postReplacer; + + public HttpHeadersTransformationMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository, + IHttpContextRequestHeaderReplacer preReplacer, + IHttpResponseHeaderReplacer postReplacer) + : base(requestScopedDataRepository) + { + _next = next; + _postReplacer = postReplacer; + _preReplacer = preReplacer; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + var preFAndRs = this.DownstreamRoute.ReRoute.UpstreamHeadersFindAndReplace; + + _preReplacer.Replace(context, preFAndRs); + + await _next.Invoke(context); + + var postFAndRs = this.DownstreamRoute.ReRoute.DownstreamHeadersFindAndReplace; + + _postReplacer.Replace(HttpResponseMessage, postFAndRs); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddlewareExtensions.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddlewareExtensions.cs new file mode 100644 index 000000000..4dc08b4d9 --- /dev/null +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Headers.Middleware +{ + public static class HttpHeadersTransformationMiddlewareExtensions + { + public static IApplicationBuilder UseHttpHeadersTransformationMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Logging/AspDotNetLogger.cs b/src/Ocelot/Logging/AspDotNetLogger.cs index 8c336a18a..eae1eec5e 100644 --- a/src/Ocelot/Logging/AspDotNetLogger.cs +++ b/src/Ocelot/Logging/AspDotNetLogger.cs @@ -1,5 +1,6 @@ using System; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Internal; using Ocelot.Infrastructure.RequestData; namespace Ocelot.Logging @@ -22,21 +23,21 @@ public void LogTrace(string message, params object[] args) { var requestId = GetOcelotRequestId(); var previousRequestId = GetOcelotPreviousRequestId(); - _logger.LogTrace("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, message, args); + _logger.LogTrace("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, new FormattedLogValues(message, args).ToString()); } public void LogDebug(string message, params object[] args) { var requestId = GetOcelotRequestId(); var previousRequestId = GetOcelotPreviousRequestId(); - _logger.LogDebug("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, message, args); + _logger.LogDebug("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, new FormattedLogValues(message, args).ToString()); } public void LogInformation(string message, params object[] args) { var requestId = GetOcelotRequestId(); var previousRequestId = GetOcelotPreviousRequestId(); - _logger.LogInformation("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, message, args); + _logger.LogInformation("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, new FormattedLogValues(message, args).ToString()); } public void LogError(string message, Exception exception) @@ -50,7 +51,7 @@ public void LogError(string message, params object[] args) { var requestId = GetOcelotRequestId(); var previousRequestId = GetOcelotPreviousRequestId(); - _logger.LogError("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}", requestId, previousRequestId, message, args); + _logger.LogError("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}", requestId, previousRequestId, new FormattedLogValues(message, args).ToString()); } public void LogCritical(string message, Exception exception) @@ -66,7 +67,7 @@ private string GetOcelotRequestId() if (requestId == null || requestId.IsError) { - return $"no request id"; + return "no request id"; } return requestId.Data; @@ -78,7 +79,7 @@ private string GetOcelotPreviousRequestId() if (requestId == null || requestId.IsError) { - return $"no previous request id"; + return "no previous request id"; } return requestId.Data; diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index 466754b42..02c2394fb 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -86,12 +86,15 @@ public static async Task UseOcelot(this IApplicationBuilder // This is registered first so it can catch any errors and issue an appropriate response builder.UseResponderMiddleware(); - // Initialises downstream request - builder.UseDownstreamRequestInitialiser(); - // Then we get the downstream route information builder.UseDownstreamRouteFinderMiddleware(); + // Now we have the ds route we can transform headers and stuff? + builder.UseHttpHeadersTransformationMiddleware(); + + // Initialises downstream request + builder.UseDownstreamRequestInitialiser(); + // We check whether the request is ratelimit, and if there is no continue processing builder.UseRateLimiting(); diff --git a/src/Ocelot/Request/Builder/HttpRequestCreator.cs b/src/Ocelot/Request/Builder/HttpRequestCreator.cs index 8c3c9218b..a2dbb9396 100644 --- a/src/Ocelot/Request/Builder/HttpRequestCreator.cs +++ b/src/Ocelot/Request/Builder/HttpRequestCreator.cs @@ -14,7 +14,7 @@ public async Task> Build( bool useCookieContainer, bool allowAutoRedirect) { - return new OkResponse(new Request(httpRequestMessage, isQos, qosProvider, useCookieContainer, allowAutoRedirect)); + return new OkResponse(new Request(httpRequestMessage, isQos, qosProvider, allowAutoRedirect, useCookieContainer)); } } } \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs new file mode 100644 index 000000000..99320cd3f --- /dev/null +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class HeaderTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + private string _downstreamPath; + + public HeaderTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_transform_upstream_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + UpstreamHeaderTransform = new Dictionary + { + {"Laz", "D, GP"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Laz")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIAddAHeader("Laz", "D")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("GP")) + .BDDfy(); + } + + [Fact] + public void should_transform_downstream_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, http://ocelot.com/"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Location", "http://www.bbc.co.uk/")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://ocelot.com/")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_190() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 6773, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://localhost:6773, {BaseUrl}"} + }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = false + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) + .BDDfy(); + } + + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + if(context.Request.Headers.TryGetValue(headerKey, out var values)) + { + var result = values.First(); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(result); + } + }); + }) + .Build(); + + _builder.Start(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey, string headerValue) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + context.Response.OnStarting(() => { + context.Response.Headers.Add(headerKey, headerValue); + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + }); + }); + }) + .Build(); + + _builder.Start(); + } + + internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPath) + { + _downstreamPath.ShouldBe(expectedDownstreamPath); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 9cdb8e4e3..7f260d5ff 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -108,6 +108,12 @@ public void GivenOcelotIsRunning(Action opt _ocelotClient = _ocelotServer.CreateClient(); } + public void ThenTheResponseHeaderIs(string key, string value) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldBe(value); + } + public void GivenOcelotIsRunningUsingJsonSerializedCache() { _webHostBuilder = new WebHostBuilder(); @@ -326,6 +332,11 @@ public void WhenIGetUrlOnTheApiGateway(string url) _response = _ocelotClient.GetAsync(url).Result; } + public void GivenIAddAHeader(string key, string value) + { + _ocelotClient.DefaultRequestHeaders.Add(key, value); + } + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) { var tasks = new Task[times]; diff --git a/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs b/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs new file mode 100644 index 000000000..e1efa9546 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Consul; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class TwoDownstreamServicesTests : IDisposable + { + private IWebHost _builderOne; + private IWebHost _builderTwo; + private IWebHost _fakeConsulBuilder; + private readonly Steps _steps; + private readonly List _serviceEntries; + private string _downstreamPathOne; + private string _downstreamPathTwo; + + public TwoDownstreamServicesTests() + { + _steps = new Steps(); + _serviceEntries = new List(); + } + + [Fact] + public void should_fix_issue_194() + { + var consulPort = 8503; + var downstreamServiceOneUrl = "http://localhost:8362"; + var downstreamServiceTwoUrl = "http://localhost:8330"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + +// http://localhost:8362/api/user/info?id=1 is ok +// http://localhost:3164/api/user/info?id=1 is ok +// http://localhost:8330/api/product/info?id=1 is ok +// http://localhost:3164/api/product/info?id=1 is 404 + +// is my configuration.json +// { +// "ReRoutes": [ +// //{ +// // "DownstreamPathTemplate": "/{product}", +// // "DownstreamScheme": "http", +// // "UpstreamPathTemplate": "/{product}", +// // "UpstreamHttpMethod": [ "Get", "Post" ], +// // "ServiceName": "api-product", +// // "LoadBalancer": "LeastConnection", +// // "UseServiceDiscovery": true +// //}, +// //{ +// // "DownstreamPathTemplate": "/{user}", +// // "DownstreamScheme": "http", +// // "UpstreamPathTemplate": "/{user}", +// // "UpstreamHttpMethod": [ "Get", "Post" ], +// // "ServiceName": "api-user", +// // "LoadBalancer": "LeastConnection", +// // "UseServiceDiscovery": true +// //}, +// { +// "DownstreamPathTemplate": "/api/user/{user}", +// "DownstreamScheme": "http", +// "DownstreamHost": "localhost", +// "DownstreamPort": 8362, +// "UpstreamPathTemplate": "/api/user/{user}", +// "UpstreamHttpMethod": [ "Get" ] +// }, +// { +// "DownstreamPathTemplate": "/api/product/{product}", +// "DownstreamScheme": "http", +// "DownstreamHost": "localhost", +// "DownstreamPort": 8330, +// "UpstreamPathTemplate": "//api/product/{product}", +// "UpstreamHttpMethod": [ "Get" ] +// } +// ], +// "GlobalConfiguration": { +// "ServiceDiscoveryProvider": { +// "Host": "localhost", +// "Port": 8500 +// } +// } +// } + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/user/{user}", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 8362, + UpstreamPathTemplate = "/api/user/{user}", + UpstreamHttpMethod = new List { "Get" }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/api/product/{product}", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 8330, + UpstreamPathTemplate = "/api/product/{product}", + UpstreamHttpMethod = new List { "Get" }, + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() + { + Host = "localhost", + Port = consulPort + } + } + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, "/api/user/info", 200, "user")) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, "/api/product/info", 200, "product")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/user/info?id=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("user")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/product/info?id=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("product")) + .BDDfy(); + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if(context.Request.Path.Value == "/v1/health/service/product") + { + await context.Response.WriteJsonAsync(_serviceEntries); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + + private void GivenProductServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) + { + _builderOne = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if(_downstreamPathOne != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _builderOne.Start(); + } + + private void GivenProductServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) + { + _builderTwo = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if(_downstreamPathTwo != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _builderTwo.Start(); + } + + public void Dispose() + { + _builderOne?.Dispose(); + _builderTwo?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index d39fa394d..807bb30e1 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -39,6 +39,7 @@ public class FileConfigurationCreatorTests private Mock _regionCreator; private Mock _httpHandlerOptionsCreator; private Mock _adminPath; + private readonly Mock _headerFindAndReplaceCreator; public FileConfigurationCreatorTests() { @@ -56,6 +57,7 @@ public FileConfigurationCreatorTests() _regionCreator = new Mock(); _httpHandlerOptionsCreator = new Mock(); _adminPath = new Mock(); + _headerFindAndReplaceCreator = new Mock(); _ocelotConfigurationCreator = new FileOcelotConfigurationCreator( _fileConfig.Object, @@ -71,7 +73,8 @@ public FileConfigurationCreatorTests() _rateLimitOptions.Object, _regionCreator.Object, _httpHandlerOptionsCreator.Object, - _adminPath.Object); + _adminPath.Object, + _headerFindAndReplaceCreator.Object); } [Fact] @@ -91,6 +94,7 @@ public void should_call_service_provider_config_creator() } })) .And(x => x.GivenTheFollowingIsReturned(serviceProviderConfig)) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheConfigIsValid()) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheServiceProviderCreatorIsCalledCorrectly()) @@ -121,10 +125,12 @@ public void should_call_region_creator() }, })) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheFollowingRegionIsReturned("region")) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheRegionCreatorIsCalledCorrectly("region")) + .And(x => x.ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly()) .BDDfy(); } @@ -148,6 +154,7 @@ public void should_call_rate_limit_options_creator() }, })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheRateLimitOptionsCreatorIsCalledCorrectly()) @@ -187,6 +194,7 @@ public void should_call_qos_options_creator() }, })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(serviceOptions)) .And(x => x.GivenTheQosOptionsCreatorReturns(expected)) .When(x => x.WhenICreateTheConfig()) @@ -214,6 +222,7 @@ public void should_use_downstream_host() }, })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(new List @@ -248,6 +257,7 @@ public void should_use_downstream_scheme() }, })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(new List @@ -290,6 +300,7 @@ public void should_use_service_discovery_for_downstream_service_host() } })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(new List @@ -325,6 +336,7 @@ public void should_not_use_service_discovery_for_downstream_host_url_when_no_ser } })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(new List @@ -359,6 +371,7 @@ public void should_call_template_pattern_creator_correctly() } })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheUpstreamTemplatePatternCreatorReturns("(?i)/api/products/.*/$")) .When(x => x.WhenICreateTheConfig()) @@ -398,6 +411,7 @@ public void should_call_request_id_creator() } })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheRequestIdCreatorReturns("blahhhh")) .When(x => x.WhenICreateTheConfig()) @@ -435,6 +449,7 @@ public void should_call_httpHandler_creator() }, })) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheFollowingHttpHandlerOptionsAreReturned(httpHandlerOptions)) .When(x => x.WhenICreateTheConfig()) @@ -470,6 +485,7 @@ public void should_create_with_headers_to_extract(FileConfiguration fileConfig) this.Given(x => x.GivenTheConfigIs(fileConfig)) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheClaimsToThingCreatorReturns(new List { new ClaimToThing("CustomerId", "CustomerId", "", 0) })) @@ -504,6 +520,7 @@ public void should_create_with_authentication_properties(FileConfiguration fileC this.Given(x => x.GivenTheConfigIs(fileConfig)) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions)) .When(x => x.WhenICreateTheConfig()) @@ -661,6 +678,17 @@ private void ThenTheServiceProviderCreatorIsCalledCorrectly() .Verify(x => x.Create(_fileConfiguration.GlobalConfiguration), Times.Once); } + private void ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly() + { + _headerFindAndReplaceCreator + .Verify(x => x.Create(It.IsAny()), Times.Once); + } + + private void GivenTheHeaderFindAndReplaceCreatorReturns() + { + _headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny())).Returns(new HeaderTransformations(new List(), new List())); + } + private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration) { _serviceProviderConfigCreator diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs new file mode 100644 index 000000000..ceac035eb --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Middleware; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class HeaderFindAndReplaceCreatorTests + { + private HeaderFindAndReplaceCreator _creator; + private FileReRoute _reRoute; + private HeaderTransformations _result; + private Mock _finder; + + public HeaderFindAndReplaceCreatorTests() + { + _finder = new Mock(); + _creator = new HeaderFindAndReplaceCreator(_finder.Object); + } + + [Fact] + public void should_create() + { + var reRoute = new FileReRoute + { + UpstreamHeaderTransform = new Dictionary + { + {"Test", "Test, Chicken"}, + + {"Moop", "o, a"} + }, + DownstreamHeaderTransform = new Dictionary + { + {"Pop", "West, East"}, + + {"Bop", "e, r"} + } + }; + + var upstream = new List + { + new HeaderFindAndReplace("Test", "Test", "Chicken", 0), + new HeaderFindAndReplace("Moop", "o", "a", 0) + }; + + var downstream = new List + { + new HeaderFindAndReplace("Pop", "West", "East", 0), + new HeaderFindAndReplace("Bop", "e", "r", 0) + }; + + this.Given(x => GivenTheReRoute(reRoute)) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingUpstreamIsReturned(upstream)) + .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) + .BDDfy(); + } + + [Fact] + public void should_use_base_url_placeholder() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + } + }; + + var downstream = new List + { + new HeaderFindAndReplace("Location", "http://www.bbc.co.uk/", "http://ocelot.com/", 0), + }; + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => GivenTheBaseUrlIs("http://ocelot.com/")) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) + .BDDfy(); + } + + + [Fact] + public void should_use_base_url_partial_placeholder() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/pay, {BaseUrl}pay"}, + } + }; + + var downstream = new List + { + new HeaderFindAndReplace("Location", "http://www.bbc.co.uk/pay", "http://ocelot.com/pay", 0), + }; + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => GivenTheBaseUrlIs("http://ocelot.com/")) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) + .BDDfy(); + } + + private void GivenTheBaseUrlIs(string baseUrl) + { + _finder.Setup(x => x.Find()).Returns(baseUrl); + } + + private void ThenTheFollowingDownstreamIsReturned(List downstream) + { + _result.Downstream.Count.ShouldBe(downstream.Count); + + for (int i = 0; i < _result.Downstream.Count; i++) + { + var result = _result.Downstream[i]; + var expected = downstream[i]; + result.Find.ShouldBe(expected.Find); + result.Index.ShouldBe(expected.Index); + result.Key.ShouldBe(expected.Key); + result.Replace.ShouldBe(expected.Replace); + } + } + + private void GivenTheReRoute(FileReRoute reRoute) + { + _reRoute = reRoute; + } + + private void WhenICreate() + { + _result = _creator.Create(_reRoute); + } + + private void ThenTheFollowingUpstreamIsReturned(List expecteds) + { + _result.Upstream.Count.ShouldBe(expecteds.Count); + + for (int i = 0; i < _result.Upstream.Count; i++) + { + var result = _result.Upstream[i]; + var expected = expecteds[i]; + result.Find.ShouldBe(expected.Find); + result.Index.ShouldBe(expected.Index); + result.Key.ShouldBe(expected.Key); + result.Replace.ShouldBe(expected.Replace); + } + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs new file mode 100644 index 000000000..26e0b1e25 --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs @@ -0,0 +1,91 @@ +using Xunit; +using Shouldly; +using Ocelot.Headers.Middleware; +using TestStack.BDDfy; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Ocelot.Responses; +using Ocelot.Configuration; +using Ocelot.Headers; + +namespace Ocelot.UnitTests.Headers +{ + public class HttpContextRequestHeaderReplacerTests + { + private HttpContext _context; + private List _fAndRs; + private HttpContextRequestHeaderReplacer _replacer; + private Response _result; + + public HttpContextRequestHeaderReplacerTests() + { + _replacer = new HttpContextRequestHeaderReplacer(); + } + + [Fact] + public void should_replace_headers() + { + var context = new DefaultHttpContext(); + context.Request.Headers.Add("test", "test"); + + var fAndRs = new List(); + fAndRs.Add(new HeaderFindAndReplace("test", "test", "chiken", 0)); + + this.Given(x => GivenTheFollowingHttpRequest(context)) + .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) + .When(x => WhenICallTheReplacer()) + .Then(x => ThenTheHeadersAreReplaced()) + .BDDfy(); + } + + [Fact] + public void should_not_replace_headers() + { + var context = new DefaultHttpContext(); + context.Request.Headers.Add("test", "test"); + + var fAndRs = new List(); + + this.Given(x => GivenTheFollowingHttpRequest(context)) + .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) + .When(x => WhenICallTheReplacer()) + .Then(x => ThenTheHeadersAreNotReplaced()) + .BDDfy(); + } + + private void ThenTheHeadersAreNotReplaced() + { + _result.ShouldBeOfType(); + foreach (var f in _fAndRs) + { + _context.Request.Headers.TryGetValue(f.Key, out var values); + values[f.Index].ShouldBe("test"); + } + } + + private void GivenTheFollowingHttpRequest(HttpContext context) + { + _context = context; + } + + private void GivenTheFollowingHeaderReplacements(List fAndRs) + { + _fAndRs = fAndRs; + } + + private void WhenICallTheReplacer() + { + _result = _replacer.Replace(_context, _fAndRs); + } + + private void ThenTheHeadersAreReplaced() + { + _result.ShouldBeOfType(); + foreach (var f in _fAndRs) + { + _context.Request.Headers.TryGetValue(f.Key, out var values); + values[f.Index].ShouldBe(f.Replace); + } + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs new file mode 100644 index 000000000..25be507b9 --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -0,0 +1,93 @@ +using Xunit; +using Shouldly; +using Ocelot.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Ocelot.Headers.Middleware; +using TestStack.BDDfy; +using System.Linq; +using System.Threading.Tasks; +using System; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Moq; +using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder; +using Ocelot.Responses; +using Ocelot.Configuration.Builder; +using Ocelot.Headers; +using System.Net.Http; + +namespace Ocelot.UnitTests.Headers +{ + public class HttpHeadersTransformationMiddlewareTests : ServerHostedMiddlewareTest + { + private Mock _preReplacer; + private Mock _postReplacer; + + public HttpHeadersTransformationMiddlewareTests() + { + _preReplacer = new Mock(); + _postReplacer = new Mock(); + + GivenTheTestServerIsConfigured(); + } + + [Fact] + public void should_call_pre_and_post_header_transforms() + { + this.Given(x => GivenTheFollowingRequest()) + .And(x => GivenTheReRouteHasPreFindAndReplaceSetUp()) + .And(x => GivenTheHttpResponseMessageIs()) + .When(x => WhenICallTheMiddleware()) + .Then(x => ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly()) + .And(x => ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly()) + .BDDfy(); + } + + private void GivenTheHttpResponseMessageIs() + { + var httpResponseMessage = new HttpResponseMessage(); + var response = new OkResponse(httpResponseMessage); + ScopedRepository.Setup(x => x.Get("HttpResponseMessage")).Returns(response); + } + + private void GivenTheReRouteHasPreFindAndReplaceSetUp() + { + var fAndRs = new List(); + var reRoute = new ReRouteBuilder().WithUpstreamHeaderFindAndReplace(fAndRs).WithDownstreamHeaderFindAndReplace(fAndRs).Build(); + var dR = new DownstreamRoute(null, reRoute); + var response = new OkResponse(dR); + ScopedRepository.Setup(x => x.Get("DownstreamRoute")).Returns(response); + } + + private void ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly() + { + _preReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); + } + + private void ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly() + { + _postReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); + } + + private void GivenTheFollowingRequest() + { + Client.DefaultRequestHeaders.Add("test", "test"); + } + + protected override void GivenTheTestServerServicesAreConfigured(IServiceCollection services) + { + services.AddSingleton(); + services.AddLogging(); + services.AddSingleton(ScopedRepository.Object); + services.AddSingleton(_preReplacer.Object); + services.AddSingleton(_postReplacer.Object); + } + + protected override void GivenTheTestServerPipelineIsConfigured(IApplicationBuilder app) + { + app.UseHttpHeadersTransformationMiddleware(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs new file mode 100644 index 000000000..8d7c5a226 --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs @@ -0,0 +1,91 @@ +using Xunit; +using Shouldly; +using TestStack.BDDfy; +using System.Net.Http; +using Ocelot.Headers; +using Ocelot.Configuration; +using System.Collections.Generic; +using Ocelot.Responses; +using System.Linq; + +namespace Ocelot.UnitTests.Headers +{ + public class HttpResponseHeaderReplacerTests + { + private HttpResponseMessage _response; + private HttpResponseHeaderReplacer _replacer; + private List _headerFindAndReplaces; + private Response _result; + + public HttpResponseHeaderReplacerTests() + { + _replacer = new HttpResponseHeaderReplacer(); + } + [Fact] + public void should_replace_headers() + { + var response = new HttpResponseMessage(); + response.Headers.Add("test", "test"); + + var fAndRs = new List(); + fAndRs.Add(new HeaderFindAndReplace("test", "test", "chiken", 0)); + + this.Given(x => GivenTheHttpResponse(response)) + .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) + .When(x => WhenICallTheReplacer()) + .Then(x => ThenTheHeadersAreReplaced()) + .BDDfy(); + } + + [Fact] + public void should_not_replace_headers() + { + var response = new HttpResponseMessage(); + response.Headers.Add("test", "test"); + + var fAndRs = new List(); + + this.Given(x => GivenTheHttpResponse(response)) + .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) + .When(x => WhenICallTheReplacer()) + .Then(x => ThenTheHeadersAreNotReplaced()) + .BDDfy(); + } + + + private void ThenTheHeadersAreNotReplaced() + { + _result.ShouldBeOfType(); + foreach (var f in _headerFindAndReplaces) + { + _response.Headers.TryGetValues(f.Key, out var values); + values.ToList()[f.Index].ShouldBe("test"); + } + } + + private void GivenTheFollowingHeaderReplacements(List fAndRs) + { + _headerFindAndReplaces = fAndRs; + } + + private void GivenTheHttpResponse(HttpResponseMessage response) + { + _response = response; + } + + private void WhenICallTheReplacer() + { + _result = _replacer.Replace(_response, _headerFindAndReplaces); + } + + private void ThenTheHeadersAreReplaced() + { + _result.ShouldBeOfType(); + foreach (var f in _headerFindAndReplaces) + { + _response.Headers.TryGetValues(f.Key, out var values); + values.ToList()[f.Index].ShouldBe(f.Replace); + } + } + } +} \ No newline at end of file diff --git a/tools/packages.config b/tools/packages.config index 747e13e64..e52a2c7e9 100644 --- a/tools/packages.config +++ b/tools/packages.config @@ -1,4 +1,4 @@ - +