diff --git a/docs/features/routing.rst b/docs/features/routing.rst index 18cc60c18..b602a8726 100644 --- a/docs/features/routing.rst +++ b/docs/features/routing.rst @@ -3,9 +3,9 @@ Routing Ocelot's primary functionality is to take incoming http requests and forward them on to a downstream service. Ocelot currently only supports this in the form of another http request (in the future -this could be any transport mechanism). +this could be any transport mechanism). -Ocelot's describes the routing of one request to another as a ReRoute. In order to get +Ocelot's describes the routing of one request to another as a ReRoute. In order to get anything working in Ocelot you need to set up a ReRoute in the configuration. .. code-block:: json @@ -32,18 +32,18 @@ To configure a ReRoute you need to add one to the ReRoutes json array. "UpstreamHttpMethod": [ "Put", "Delete" ] } -The DownstreamPathTemplate, DownstreamScheme and DownstreamHostAndPorts define the URL that a request will be forwarded to. +The DownstreamPathTemplate, DownstreamScheme and DownstreamHostAndPorts define the URL that a request will be forwarded to. -DownstreamHostAndPorts is a collection that defines the host and port of any downstream services that you wish to forward requests to. +DownstreamHostAndPorts is a collection that defines the host and port of any downstream services that you wish to forward requests to. Usually this will just contain a single entry but sometimes you might want to load balance requests to your downstream services and Ocelot allows you add more than one entry and then select a load balancer. -The UpstreamPathTemplate is the URL that Ocelot will use to identify which DownstreamPathTemplate to use for a given request. -The UpstreamHttpMethod is used so Ocelot can distinguish between requests with different HTTP verbs to the same URL. You can set a specific list of HTTP Methods or set an empty list to allow any of them. +The UpstreamPathTemplate is the URL that Ocelot will use to identify which DownstreamPathTemplate to use for a given request. +The UpstreamHttpMethod is used so Ocelot can distinguish between requests with different HTTP verbs to the same URL. You can set a specific list of HTTP Methods or set an empty list to allow any of them. In Ocelot you can add placeholders for variables to your Templates in the form of {something}. The placeholder variable needs to be present in both the DownstreamPathTemplate and UpstreamPathTemplate properties. When it is Ocelot will attempt to substitute the value in the UpstreamPathTemplate placeholder into the DownstreamPathTemplate for each request Ocelot processes. -You can also do a catch all type of ReRoute e.g. +You can also do a catch all type of ReRoute e.g. .. code-block:: json @@ -72,7 +72,7 @@ In order to change this you can specify on a per ReRoute basis the following set "ReRouteIsCaseSensitive": true This means that when Ocelot tries to match the incoming upstream url with an upstream template the -evaluation will be case sensitive. +evaluation will be case sensitive. Catch All ^^^^^^^^^ @@ -96,7 +96,7 @@ If you set up your config like below, all requests will be proxied straight thro "UpstreamHttpMethod": [ "Get" ] } -The catch all has a lower priority than any other ReRoute. If you also have the ReRoute below in your config then Ocelot would match it before the catch all. +The catch all has a lower priority than any other ReRoute. If you also have the ReRoute below in your config then Ocelot would match it before the catch all. .. code-block:: json @@ -113,7 +113,7 @@ The catch all has a lower priority than any other ReRoute. If you also have the "UpstreamHttpMethod": [ "Get" ] } -Upstream Host +Upstream Host ^^^^^^^^^^^^^ This feature allows you to have ReRoutes based on the upstream host. This works by looking at the host header the client has used and then using this as part of the information we use to identify a ReRoute. @@ -138,7 +138,7 @@ In order to use this feature please add the following to your config. The ReRoute above will only be matched when the host header value is somedomain.com. -If you do not set UpstreamHost on a ReRoute then any host header will match it. This means that if you have two ReRoutes that are the same, apart from the UpstreamHost, where one is null and the other set Ocelot will favour the one that has been set. +If you do not set UpstreamHost on a ReRoute then any host header will match it. This means that if you have two ReRoutes that are the same, apart from the UpstreamHost, where one is null and the other set Ocelot will favour the one that has been set. This feature was requested as part of `Issue 216 `_ . @@ -166,7 +166,7 @@ e.g. you could have "Priority": 0 } -and +and .. code-block:: json @@ -181,9 +181,9 @@ matched /goods/{catchAll} (because this is the first ReRoute in the list!). Dynamic Routing ^^^^^^^^^^^^^^^ -This feature was requested in `issue 340 `_. +This feature was requested in `issue 340 `_. -The idea is to enable dynamic routing when using a service discovery provider so you don't have to provide the ReRoute config. See the docs :ref:`service-discovery` if +The idea is to enable dynamic routing when using a service discovery provider so you don't have to provide the ReRoute config. See the docs :ref:`service-discovery` if this sounds interesting to you. Query Strings @@ -241,5 +241,38 @@ Ocelot will also allow you to put query string parameters in the UpstreamPathTem } } -In this example Ocelot will only match requests that have a matching url path and the query string starts with unitId=something. You can have other queries after this -but you must start with the matching parameter. Also Ocelot will swap the {unitId} parameter from the query string and use it in the downstream request path. +In this example Ocelot will only match requests that have a matching url path and the query string starts with unitId=something. You can have other queries after this but you must start with the matching parameter. Also Ocelot will swap the {unitId} parameter from the query string and use it in the downstream request path. + +Upstream header-based routing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This feature was requested in `issue 360 `_ and `issue 624 `_. + +Ocelot allows you to define a ReRoute with upstream headers, each of which may define a set of accepted values. If a ReRoute has a set of upstream headers defined in it, it will no longer match a request's upstream path based solely on upstream path template. The request must also contain one or more headers required by the ReRoute for a match. + +A sample configuration might look like the following: + +.. code-block:: json + + { + "ReRoutes": [ + { + "UpstreamHeaderRoutingOptions": { + "Headers": { + "X-API-Version": [ "1", "2" ], + "X-Tennant-Id": [ "tennantId" ] + }, + "CombinationMode": "all" + } + } + ] + } + +The ``UpstreamHeaderRoutingOptions`` block defines two attributes -- the ``Headers`` block and the ``CombinationMode`` attribute. The ``Headers`` attribute defines required header names as keys and lists of acceptable header values as values. During route matching, both header names and values are matched in *case insensitive* manner. Please note that if a header has more than one acceptable value configured, presence of any of those values in a request is sufficient for a header to be a match. + +The second attribute, ``CombinationMode``, defines how the route finder will determine whether a particular header configuration in a request matches a ReRoute's header configuration. The attribute accepts two values: + +* ``"Any"`` causes the route finder to match a ReRoute if any value of any configured header is present in a request +* ``"All"`` causes the route finder to match a ReRoute only if any value of *all* configured headers is present in a request + +The value for this attribute is case-insensitive and, if not specified, ``"Any"`` is used as the default. diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index 4716c03af..e9ff7ebe2 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -1,11 +1,11 @@ namespace Ocelot.Configuration.Builder { - using Ocelot.Configuration.File; + using Ocelot.Configuration.File; using Ocelot.Values; using System.Collections.Generic; - using System.Linq; + using System.Linq; using System.Net.Http; - + public class ReRouteBuilder { private UpstreamPathTemplate _upstreamTemplatePattern; @@ -14,6 +14,7 @@ public class ReRouteBuilder private List _downstreamReRoutes; private List _downstreamReRoutesConfig; private string _aggregator; + private UpstreamHeaderRoutingOptions _upstreamHeaderRoutingOptions; public ReRouteBuilder() { @@ -55,24 +56,30 @@ public ReRouteBuilder WithAggregateReRouteConfig(List ag { _downstreamReRoutesConfig = aggregateReRouteConfigs; return this; - } - + } + public ReRouteBuilder WithAggregator(string aggregator) { _aggregator = aggregator; return this; } + public ReRouteBuilder WithUpstreamHeaderRoutingOptions(UpstreamHeaderRoutingOptions routingOptions) + { + _upstreamHeaderRoutingOptions = routingOptions; + return this; + } + public ReRoute Build() { return new ReRoute( - _downstreamReRoutes, + _downstreamReRoutes, _downstreamReRoutesConfig, - _upstreamHttpMethod, - _upstreamTemplatePattern, + _upstreamHttpMethod, + _upstreamTemplatePattern, _upstreamHost, - _aggregator - ); + _aggregator, + _upstreamHeaderRoutingOptions); } } -} +} diff --git a/src/Ocelot/Configuration/Creator/IUpstreamHeaderRoutingOptionsCreator.cs b/src/Ocelot/Configuration/Creator/IUpstreamHeaderRoutingOptionsCreator.cs new file mode 100644 index 000000000..8c965d08f --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IUpstreamHeaderRoutingOptionsCreator.cs @@ -0,0 +1,9 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public interface IUpstreamHeaderRoutingOptionsCreator + { + UpstreamHeaderRoutingOptions Create(FileUpstreamHeaderRoutingOptions options); + } +} diff --git a/src/Ocelot/Configuration/Creator/ReRoutesCreator.cs b/src/Ocelot/Configuration/Creator/ReRoutesCreator.cs index 52b819852..5f746aeac 100644 --- a/src/Ocelot/Configuration/Creator/ReRoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/ReRoutesCreator.cs @@ -22,6 +22,7 @@ public class ReRoutesCreator : IReRoutesCreator private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; private readonly IReRouteKeyCreator _reRouteKeyCreator; private readonly ISecurityOptionsCreator _securityOptionsCreator; + private readonly IUpstreamHeaderRoutingOptionsCreator _upstreamHeaderRoutingOptionsCreator; public ReRoutesCreator( IClaimsToThingCreator claimsToThingCreator, @@ -37,7 +38,8 @@ public ReRoutesCreator( IDownstreamAddressesCreator downstreamAddressesCreator, ILoadBalancerOptionsCreator loadBalancerOptionsCreator, IReRouteKeyCreator reRouteKeyCreator, - ISecurityOptionsCreator securityOptionsCreator + ISecurityOptionsCreator securityOptionsCreator, + IUpstreamHeaderRoutingOptionsCreator upstreamHeaderRoutingOptionsCreator ) { _reRouteKeyCreator = reRouteKeyCreator; @@ -55,6 +57,7 @@ ISecurityOptionsCreator securityOptionsCreator _httpHandlerOptionsCreator = httpHandlerOptionsCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _securityOptionsCreator = securityOptionsCreator; + _upstreamHeaderRoutingOptionsCreator = upstreamHeaderRoutingOptionsCreator; } public List Create(FileConfiguration fileConfiguration) @@ -144,11 +147,14 @@ private ReRoute SetUpReRoute(FileReRoute fileReRoute, DownstreamReRoute downstre { var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileReRoute); + var upstreamHeaderRoutingOptions = _upstreamHeaderRoutingOptionsCreator.Create(fileReRoute.UpstreamHeaderRoutingOptions); + var reRoute = new ReRouteBuilder() .WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod) .WithUpstreamPathTemplate(upstreamTemplatePattern) .WithDownstreamReRoute(downstreamReRoutes) .WithUpstreamHost(fileReRoute.UpstreamHost) + .WithUpstreamHeaderRoutingOptions(upstreamHeaderRoutingOptions) .Build(); return reRoute; diff --git a/src/Ocelot/Configuration/Creator/UpstreamHeaderRoutingOptionsCreator.cs b/src/Ocelot/Configuration/Creator/UpstreamHeaderRoutingOptionsCreator.cs new file mode 100644 index 000000000..12270982b --- /dev/null +++ b/src/Ocelot/Configuration/Creator/UpstreamHeaderRoutingOptionsCreator.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public class UpstreamHeaderRoutingOptionsCreator : IUpstreamHeaderRoutingOptionsCreator + { + public UpstreamHeaderRoutingOptions Create(FileUpstreamHeaderRoutingOptions options) + { + UpstreamHeaderRoutingCombinationMode mode = UpstreamHeaderRoutingCombinationMode.Any; + if (options.CombinationMode.Length > 0) + { + mode = (UpstreamHeaderRoutingCombinationMode) + Enum.Parse(typeof(UpstreamHeaderRoutingCombinationMode), options.CombinationMode, true); + } + + Dictionary> headers = options.Headers.ToDictionary( + kv => kv.Key.ToLowerInvariant(), + kv => new HashSet(kv.Value.Select(v => v.ToLowerInvariant()))); + + return new UpstreamHeaderRoutingOptions(headers, mode); + } + } +} diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index bad0e2af3..7560dcd5d 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -22,6 +22,7 @@ public FileReRoute() DelegatingHandlers = new List(); LoadBalancerOptions = new FileLoadBalancerOptions(); SecurityOptions = new FileSecurityOptions(); + UpstreamHeaderRoutingOptions = new FileUpstreamHeaderRoutingOptions(); Priority = 1; } @@ -53,5 +54,6 @@ public FileReRoute() public int Timeout { get; set; } public bool DangerousAcceptAnyServerCertificateValidator { get; set; } public FileSecurityOptions SecurityOptions { get; set; } + public FileUpstreamHeaderRoutingOptions UpstreamHeaderRoutingOptions { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileUpstreamHeaderRoutingOptions.cs b/src/Ocelot/Configuration/File/FileUpstreamHeaderRoutingOptions.cs new file mode 100644 index 000000000..e162fbe78 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileUpstreamHeaderRoutingOptions.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration.File +{ + public class FileUpstreamHeaderRoutingOptions + { + public FileUpstreamHeaderRoutingOptions() + { + Headers = new Dictionary>(); + CombinationMode = ""; + } + + public Dictionary> Headers { get; set; } + + public string CombinationMode { get; set; } + } +} diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index e7efe12bc..577241adf 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -9,10 +9,11 @@ public class ReRoute { public ReRoute(List downstreamReRoute, List downstreamReRouteConfig, - List upstreamHttpMethod, - UpstreamPathTemplate upstreamTemplatePattern, + List upstreamHttpMethod, + UpstreamPathTemplate upstreamTemplatePattern, string upstreamHost, - string aggregator) + string aggregator, + UpstreamHeaderRoutingOptions upstreamHeaderRoutingOptions) { UpstreamHost = upstreamHost; DownstreamReRoute = downstreamReRoute; @@ -20,6 +21,7 @@ public ReRoute(List downstreamReRoute, UpstreamHttpMethod = upstreamHttpMethod; UpstreamTemplatePattern = upstreamTemplatePattern; Aggregator = aggregator; + UpstreamHeaderRoutingOptions = upstreamHeaderRoutingOptions; } public UpstreamPathTemplate UpstreamTemplatePattern { get; private set; } @@ -28,5 +30,6 @@ public ReRoute(List downstreamReRoute, public List DownstreamReRoute { get; private set; } public List DownstreamReRouteConfig { get; private set; } public string Aggregator { get; private set; } + public UpstreamHeaderRoutingOptions UpstreamHeaderRoutingOptions { get; private set; } } -} +} diff --git a/src/Ocelot/Configuration/UpstreamHeaderRoutingCombinationMode.cs b/src/Ocelot/Configuration/UpstreamHeaderRoutingCombinationMode.cs new file mode 100644 index 000000000..d71151a00 --- /dev/null +++ b/src/Ocelot/Configuration/UpstreamHeaderRoutingCombinationMode.cs @@ -0,0 +1,8 @@ +namespace Ocelot.Configuration +{ + public enum UpstreamHeaderRoutingCombinationMode + { + Any = 0, + All = 1, + } +} diff --git a/src/Ocelot/Configuration/UpstreamHeaderRoutingOptions.cs b/src/Ocelot/Configuration/UpstreamHeaderRoutingOptions.cs new file mode 100644 index 000000000..b46c9072c --- /dev/null +++ b/src/Ocelot/Configuration/UpstreamHeaderRoutingOptions.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration +{ + public class UpstreamHeaderRoutingOptions + { + public UpstreamHeaderRoutingOptions(Dictionary> headers, UpstreamHeaderRoutingCombinationMode mode) + { + Headers = new UpstreamRoutingHeaders(headers); + Mode = mode; + } + + public bool Enabled() => !Headers.Empty(); + + public UpstreamRoutingHeaders Headers { get; private set; } + + public UpstreamHeaderRoutingCombinationMode Mode { get; private set; } + } +} diff --git a/src/Ocelot/Configuration/UpstreamRoutingHeaders.cs b/src/Ocelot/Configuration/UpstreamRoutingHeaders.cs new file mode 100644 index 000000000..64a5f0e48 --- /dev/null +++ b/src/Ocelot/Configuration/UpstreamRoutingHeaders.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Ocelot.Configuration +{ + public class UpstreamRoutingHeaders + { + public UpstreamRoutingHeaders(Dictionary> headers) + { + Headers = headers; + } + + public bool Empty() => Headers.Count == 0; + + public bool HasAnyOf(IHeaderDictionary requestHeaders) + { + IHeaderDictionary lowerCaseHeaders = GetLowerCaseHeaders(requestHeaders); + foreach (KeyValuePair> h in Headers) + { + if (lowerCaseHeaders.TryGetValue(h.Key, out var values)) + { + HashSet requestHeaderValues = new HashSet(values); + if (h.Value.Overlaps(requestHeaderValues)) + { + return true; + } + } + } + + return false; + } + + public bool HasAllOf(IHeaderDictionary requestHeaders) + { + IHeaderDictionary lowerCaseHeaders = GetLowerCaseHeaders(requestHeaders); + foreach (KeyValuePair> h in Headers) + { + if (!lowerCaseHeaders.TryGetValue(h.Key, out var values)) + { + return false; + } + + HashSet requestHeaderValues = new HashSet(values); + if (!h.Value.Overlaps(requestHeaderValues)) + { + return false; + } + } + + return true; + } + + private IHeaderDictionary GetLowerCaseHeaders(IHeaderDictionary headers) + { + IHeaderDictionary lowerCaseHeaders = new HeaderDictionary(); + foreach (KeyValuePair kv in headers) + { + string key = kv.Key.ToLowerInvariant(); + StringValues values = new StringValues(kv.Value.Select(v => v.ToLowerInvariant()).ToArray()); + lowerCaseHeaders.Add(key, values); + } + + return lowerCaseHeaders; + } + + public Dictionary> Headers { get; private set; } + } +} diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index c8a4cc500..627f2fa4d 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -72,6 +72,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs index ec6821adf..800ba387e 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs @@ -8,6 +8,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; + using Microsoft.AspNetCore.Http; using UrlMatcher; public class DownstreamRouteCreator : IDownstreamRouteProvider @@ -21,7 +22,13 @@ public DownstreamRouteCreator(IQoSOptionsCreator qoSOptionsCreator) _cache = new ConcurrentDictionary>(); } - public Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost) + public Response Get( + string upstreamUrlPath, + string upstreamQueryString, + string upstreamHttpMethod, + IInternalConfiguration configuration, + string upstreamHost, + IHeaderDictionary requestHeaders) { var serviceName = GetServiceName(upstreamUrlPath); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index 115fbd424..0a11fa462 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -3,6 +3,7 @@ using Ocelot.Responses; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Http; namespace Ocelot.DownstreamRouteFinder.Finder { @@ -17,12 +18,18 @@ public DownstreamRouteFinder(IUrlPathToUrlTemplateMatcher urlMatcher, IPlacehold _placeholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; } - public Response Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod, IInternalConfiguration configuration, string upstreamHost) + public Response Get( + string upstreamUrlPath, + string upstreamQueryString, + string httpMethod, + IInternalConfiguration configuration, + string upstreamHost, + IHeaderDictionary requestHeaders) { var downstreamRoutes = new List(); var applicableReRoutes = configuration.ReRoutes - .Where(r => RouteIsApplicableToThisRequest(r, httpMethod, upstreamHost)) + .Where(r => RouteIsApplicableToThisRequest(r, httpMethod, upstreamHost, requestHeaders)) .OrderByDescending(x => x.UpstreamTemplatePattern.Priority); foreach (var reRoute in applicableReRoutes) @@ -46,10 +53,32 @@ public Response Get(string upstreamUrlPath, string upstreamQuer return new ErrorResponse(new UnableToFindDownstreamRouteError(upstreamUrlPath, httpMethod)); } - private bool RouteIsApplicableToThisRequest(ReRoute reRoute, string httpMethod, string upstreamHost) + private bool RouteIsApplicableToThisRequest(ReRoute reRoute, string httpMethod, string upstreamHost, IHeaderDictionary requestHeaders) { - return (reRoute.UpstreamHttpMethod.Count == 0 || reRoute.UpstreamHttpMethod.Select(x => x.Method.ToLower()).Contains(httpMethod.ToLower())) && - (string.IsNullOrEmpty(reRoute.UpstreamHost) || reRoute.UpstreamHost == upstreamHost); + return (reRoute.UpstreamHttpMethod.Count == 0 || RouteHasHttpMethod(reRoute, httpMethod)) && + (string.IsNullOrEmpty(reRoute.UpstreamHost) || reRoute.UpstreamHost == upstreamHost) && + (reRoute.UpstreamHeaderRoutingOptions == null || !reRoute.UpstreamHeaderRoutingOptions.Enabled() || RouteHasRequiredUpstreamHeaders(reRoute, requestHeaders)); + } + + private bool RouteHasHttpMethod(ReRoute reRoute, string httpMethod) + { + return reRoute.UpstreamHttpMethod.Select(x => x.Method.ToLower()).Contains(httpMethod.ToLower()); + } + + private bool RouteHasRequiredUpstreamHeaders(ReRoute reRoute, IHeaderDictionary requestHeaders) + { + bool result = false; + switch (reRoute.UpstreamHeaderRoutingOptions.Mode) + { + case UpstreamHeaderRoutingCombinationMode.Any: + result = reRoute.UpstreamHeaderRoutingOptions.Headers.HasAnyOf(requestHeaders); + break; + case UpstreamHeaderRoutingCombinationMode.All: + result = reRoute.UpstreamHeaderRoutingOptions.Headers.HasAllOf(requestHeaders); + break; + } + + return result; } private DownstreamRoute GetPlaceholderNamesAndValues(string path, string query, ReRoute reRoute) diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs index b2809ac9a..307ed7fbd 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs @@ -1,10 +1,17 @@ using Ocelot.Configuration; using Ocelot.Responses; +using Microsoft.AspNetCore.Http; namespace Ocelot.DownstreamRouteFinder.Finder { public interface IDownstreamRouteProvider { - Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost); + Response Get( + string upstreamUrlPath, + string upstreamQueryString, + string upstreamHttpMethod, + IInternalConfiguration configuration, + string upstreamHost, + IHeaderDictionary requestHeaders); } } diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index 8ac0705cd..dcb777373 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -37,7 +37,11 @@ public async Task Invoke(DownstreamContext context) var provider = _factory.Get(context.Configuration); - var downstreamRoute = provider.Get(upstreamUrlPath, upstreamQueryString, context.HttpContext.Request.Method, context.Configuration, upstreamHost); + var downstreamRoute = provider.Get( + upstreamUrlPath, upstreamQueryString, + context.HttpContext.Request.Method, + context.Configuration, upstreamHost, + context.HttpContext.Request.Headers); if (downstreamRoute.IsError) { diff --git a/test/Ocelot.UnitTests/Configuration/ReRoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ReRoutesCreatorTests.cs index 32c235aa5..aff15bb87 100644 --- a/test/Ocelot.UnitTests/Configuration/ReRoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ReRoutesCreatorTests.cs @@ -30,6 +30,7 @@ public class ReRoutesCreatorTests private Mock _lboCreator; private Mock _rrkCreator; private Mock _soCreator; + private Mock _uhroCreator; private FileConfiguration _fileConfig; private ReRouteOptions _rro; private string _requestId; @@ -46,6 +47,7 @@ public class ReRoutesCreatorTests private LoadBalancerOptions _lbo; private List _result; private SecurityOptions _securityOptions; + private UpstreamHeaderRoutingOptions _upstreamHeaderRoutingOptions; public ReRoutesCreatorTests() { @@ -63,6 +65,7 @@ public ReRoutesCreatorTests() _lboCreator = new Mock(); _rrkCreator = new Mock(); _soCreator = new Mock(); + _uhroCreator = new Mock(); _creator = new ReRoutesCreator( _cthCreator.Object, @@ -78,7 +81,8 @@ public ReRoutesCreatorTests() _daCreator.Object, _lboCreator.Object, _rrkCreator.Object, - _soCreator.Object + _soCreator.Object, + _uhroCreator.Object ); } diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamHeaderRoutingOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamHeaderRoutingOptionsCreatorTests.cs new file mode 100644 index 000000000..653c39ae0 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/UpstreamHeaderRoutingOptionsCreatorTests.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration; +using Xunit; +using TestStack.BDDfy; +using Shouldly; + +namespace Ocelot.UnitTests.Configuration +{ + public class UpstreamHeaderRoutingOptionsCreatorTests + { + private FileUpstreamHeaderRoutingOptions _fileUpstreamHeaderRoutingOptions; + private IUpstreamHeaderRoutingOptionsCreator _creator; + private UpstreamHeaderRoutingOptions _upstreamHeaderRoutingOptions; + + public UpstreamHeaderRoutingOptionsCreatorTests() + { + _creator = new UpstreamHeaderRoutingOptionsCreator(); + } + + [Fact] + public void should_create_upstream_routing_header_options() + { + UpstreamHeaderRoutingOptions expected = new UpstreamHeaderRoutingOptions( + headers: new Dictionary>() + { + { "header1", new HashSet() { "value1", "value2" }}, + { "header2", new HashSet() { "value3" }}, + }, + mode: UpstreamHeaderRoutingCombinationMode.All + ); + + this.Given(_ => GivenTheseFileUpstreamHeaderRoutingOptions()) + .When(_ => WhenICreate()) + .Then(_ => ThenTheCreatedMatchesThis(expected)) + .BDDfy(); + } + + private void GivenTheseFileUpstreamHeaderRoutingOptions() + { + _fileUpstreamHeaderRoutingOptions = new FileUpstreamHeaderRoutingOptions() + { + Headers = new Dictionary>() + { + { "Header1", new List() { "Value1", "Value2" }}, + { "Header2", new List() { "Value3" }}, + }, + CombinationMode = "all", + }; + } + + private void WhenICreate() + { + _upstreamHeaderRoutingOptions = _creator.Create(_fileUpstreamHeaderRoutingOptions); + } + + private void ThenTheCreatedMatchesThis(UpstreamHeaderRoutingOptions expected) + { + _upstreamHeaderRoutingOptions.Headers.Headers.Count.ShouldBe(expected.Headers.Headers.Count); + foreach (KeyValuePair> pair in _upstreamHeaderRoutingOptions.Headers.Headers) + { + expected.Headers.Headers.TryGetValue(pair.Key, out var expectedValue).ShouldBe(true); + expectedValue.SetEquals(pair.Value).ShouldBe(true); + } + + _upstreamHeaderRoutingOptions.Mode.ShouldBe(expected.Mode); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamRoutingHeadersTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamRoutingHeadersTests.cs new file mode 100644 index 000000000..a03a3ed71 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/UpstreamRoutingHeadersTests.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Xunit; +using TestStack.BDDfy; +using Shouldly; +using Ocelot.Configuration; + +namespace Ocelot.UnitTests.Configuration +{ + public class UpstreamRoutingHeadersTests + { + private Dictionary> _headersDictionary; + private UpstreamRoutingHeaders _upstreamRoutingHeaders; + private IHeaderDictionary _requestHeaders; + + [Fact] + public void should_create_empty_headers() + { + this.Given(_ => GivenEmptyHeaderDictionary()) + .When(_ => WhenICreate()) + .Then(_ => ThenEmptyIs(true)) + .BDDfy(); + } + + [Fact] + public void should_create_preset_headers() + { + this.Given(_ => GivenPresetHeaderDictionary()) + .When(_ => WhenICreate()) + .Then(_ => ThenEmptyIs(false)) + .BDDfy(); + } + + [Fact] + public void should_not_match_mismatching_request_headers() + { + this.Given(_ => GivenPresetHeaderDictionary()) + .And(_ => AndGivenMismatchingRequestHeaders()) + .When(_ => WhenICreate()) + .Then(_ => ThenHasAnyOfIs(false)) + .And(_ => ThenHasAllOfIs(false)) + .BDDfy(); + } + + [Fact] + public void should_not_match_matching_header_with_mismatching_value() + { + this.Given(_ => GivenPresetHeaderDictionary()) + .And(_ => AndGivenOneMatchingHeaderWithMismatchingValue()) + .When(_ => WhenICreate()) + .Then(_ => ThenHasAnyOfIs(false)) + .And(_ => ThenHasAllOfIs(false)) + .BDDfy(); + } + + [Fact] + public void should_match_any_header_not_all() + { + this.Given(_ => GivenPresetHeaderDictionary()) + .And(_ => AndGivenOneMatchingHeaderWithMatchingValue()) + .When(_ => WhenICreate()) + .Then(_ => ThenHasAnyOfIs(true)) + .And(_ => ThenHasAllOfIs(false)) + .BDDfy(); + } + + [Fact] + public void should_match_any_and_all_headers() + { + this.Given(_ => GivenPresetHeaderDictionary()) + .And(_ => AndGivenTwoMatchingHeadersWithMatchingValues()) + .When(_ => WhenICreate()) + .Then(_ => ThenHasAnyOfIs(true)) + .And(_ => ThenHasAllOfIs(true)) + .BDDfy(); + } + + private void GivenEmptyHeaderDictionary() + { + _headersDictionary = new Dictionary>(); + } + + private void GivenPresetHeaderDictionary() + { + _headersDictionary = new Dictionary>() + { + { "testheader1", new HashSet() { "testheader1value1", "testheader1value2" } }, + { "testheader2", new HashSet() { "testheader1Value1", "testheader2value2" } }, + }; + } + + private void AndGivenMismatchingRequestHeaders() + { + _requestHeaders = new HeaderDictionary() { + { "someHeader", new StringValues(new string[]{ "someHeaderValue" })}, + }; + } + + private void AndGivenOneMatchingHeaderWithMismatchingValue() + { + _requestHeaders = new HeaderDictionary() { + { "testHeader1", new StringValues(new string[]{ "mismatchingValue" })}, + }; + } + + private void AndGivenOneMatchingHeaderWithMatchingValue() + { + _requestHeaders = new HeaderDictionary() { + { "testHeader1", new StringValues(new string[]{ "testHeader1Value1" })}, + }; + } + + private void AndGivenTwoMatchingHeadersWithMatchingValues() + { + _requestHeaders = new HeaderDictionary() { + { "testHeader1", new StringValues(new string[]{ "testHeader1Value1", "bogusValue" })}, + { "testHeader2", new StringValues(new string[]{ "bogusValue", "testHeader2Value2" })}, + }; + } + + private void WhenICreate() + { + _upstreamRoutingHeaders = new UpstreamRoutingHeaders(_headersDictionary); + } + + private void ThenEmptyIs(bool expected) + { + _upstreamRoutingHeaders.Empty().ShouldBe(expected); + } + + private void ThenHasAnyOfIs(bool expected) + { + _upstreamRoutingHeaders.HasAnyOf(_requestHeaders).ShouldBe(expected); + } + + private void ThenHasAllOfIs(bool expected) + { + _upstreamRoutingHeaders.HasAllOf(_requestHeaders).ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs index a3348627b..acb674e9c 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs @@ -13,6 +13,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder using System.Net.Http; using TestStack.BDDfy; using Xunit; + using Microsoft.AspNetCore.Http; public class DownstreamRouteCreatorTests { @@ -28,6 +29,7 @@ public class DownstreamRouteCreatorTests private Mock _qosOptionsCreator; private Response _resultTwo; private string _upstreamQuery; + private readonly IHeaderDictionary _upstreamHeaders; public DownstreamRouteCreatorTests() { @@ -39,6 +41,7 @@ public DownstreamRouteCreatorTests() .Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns(_qoSOptions); _creator = new DownstreamRouteCreator(_qosOptionsCreator.Object); + _upstreamHeaders = new HeaderDictionary(); } [Fact] @@ -284,12 +287,12 @@ private void ThenTheHandlerOptionsAreSet() private void WhenICreate() { - _result = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost); + _result = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); } private void WhenICreateAgain() { - _resultTwo = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost); + _resultTwo = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); } private void ThenTheDownstreamRoutesAreTheSameReference() diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index 3b3332588..51371cece 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -83,7 +83,7 @@ private void GivenTheDownStreamRouteFinderReturns(DownstreamRoute downstreamRout { _downstreamRoute = new OkResponse(downstreamRoute); _finder - .Setup(x => x.Get(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Get(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_downstreamRoute); } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index 7419fe558..2238013c7 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -8,6 +8,8 @@ using Ocelot.Values; using Shouldly; using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; using TestStack.BDDfy; using Xunit; @@ -26,6 +28,8 @@ public class DownstreamRouteFinderTests private string _upstreamHttpMethod; private string _upstreamHost; private string _upstreamQuery; + private UpstreamHeaderRoutingOptions _upstreamHeaderRoutingOptions; + private IHeaderDictionary _requestHeaders; public DownstreamRouteFinderTests() { @@ -65,6 +69,7 @@ public void should_return_highest_priority_when_first() }, string.Empty, serviceProviderConfig)) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), new ReRouteBuilder() @@ -111,6 +116,7 @@ public void should_return_highest_priority_when_lowest() }, string.Empty, serviceProviderConfig)) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), new ReRouteBuilder() @@ -150,6 +156,7 @@ public void should_return_route() )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute( @@ -192,6 +199,7 @@ public void should_not_append_slash_to_upstream_url_path() )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute( @@ -235,6 +243,7 @@ public void should_return_route_if_upstream_path_and_upstream_template_are_the_s )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), @@ -285,6 +294,7 @@ public void should_return_correct_route_for_http_verb() )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), @@ -322,9 +332,9 @@ public void should_not_return_route() )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(false)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenAnErrorResponseIsReturned()) + .Then(x => x.ThenAnErrorResponseIsReturned()) .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) .BDDfy(); } @@ -354,6 +364,7 @@ public void should_return_correct_route_for_http_verb_setting_multiple_upstream_ )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), @@ -395,6 +406,7 @@ public void should_return_correct_route_for_http_verb_setting_all_upstream_http_ )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), @@ -436,6 +448,7 @@ public void should_not_return_route_for_http_verb_not_setting_in_upstream_http_m )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenAnErrorResponseIsReturned()) .And(x => x.ThenTheUrlMatcherIsNotCalled()) @@ -468,6 +481,7 @@ public void should_return_route_when_host_matches() )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute( @@ -511,6 +525,7 @@ public void should_return_route_when_upstreamhost_is_null() )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute( @@ -563,6 +578,7 @@ public void should_not_return_route_when_host_doesnt_match() )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenAnErrorResponseIsReturned()) .And(x => x.ThenTheUrlMatcherIsNotCalled()) @@ -593,6 +609,7 @@ public void should_not_return_route_when_host_doesnt_match_with_empty_upstream_h )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenAnErrorResponseIsReturned()) .And(x => x.ThenTheUrlMatcherIsNotCalled()) @@ -623,6 +640,7 @@ public void should_return_route_when_host_does_match_with_empty_upstream_http_me )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 0)) .BDDfy(); @@ -663,6 +681,7 @@ public void should_return_route_when_host_matches_but_null_host_on_same_path_fir )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) .When(x => x.WhenICallTheFinder()) .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute( @@ -682,6 +701,115 @@ public void should_return_route_when_host_matches_but_null_host_on_same_path_fir .BDDfy(); } + [Fact] + public void should_not_return_route_with_upstream_header_routing_options_enabled_and_no_request_headers() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>( + new List()))) + .And(x => x.GivenUpstreamHeaderRoutingOptions()) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHeaderRoutingOptions(_upstreamHeaderRoutingOptions) + .Build() + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenEmptyRequestHeaders()) + .When(x => x.WhenICallTheFinder()) + .Then(x => x.ThenAnErrorResponseIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_not_return_route_with_upstream_header_routing_options_enabled_and_non_matching_request_headers() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>( + new List()))) + .And(x => x.GivenUpstreamHeaderRoutingOptions()) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHeaderRoutingOptions(_upstreamHeaderRoutingOptions) + .Build() + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenNonEmptyNonMatchingRequestHeaders()) + .When(x => x.WhenICallTheFinder()) + .Then(x => x.ThenAnErrorResponseIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_route_with_upstream_header_routing_options_enabled_and_matching_request_headers() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>( + new List()))) + .And(x => x.GivenUpstreamHeaderRoutingOptions()) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHeaderRoutingOptions(_upstreamHeaderRoutingOptions) + .Build() + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .And(x => x.GivenNonEmptyMatchingRequestHeaders()) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRoute( + new List(), + new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build() + ))) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + private void GivenTheUpstreamHostIs(string upstreamHost) { _upstreamHost = upstreamHost; @@ -699,6 +827,36 @@ private void GivenTheUpstreamHttpMethodIs(string upstreamHttpMethod) _upstreamHttpMethod = upstreamHttpMethod; } + private void GivenUpstreamHeaderRoutingOptions() + { + var headers = new Dictionary>() + { + { "header", new HashSet() { "value" }}, + }; + _upstreamHeaderRoutingOptions = new UpstreamHeaderRoutingOptions(headers, UpstreamHeaderRoutingCombinationMode.All); + } + + private void GivenEmptyRequestHeaders() + { + _requestHeaders = new HeaderDictionary(); + } + + private void GivenNonEmptyNonMatchingRequestHeaders() + { + _requestHeaders = new HeaderDictionary() + { + { "header", new StringValues(new string[]{ "mismatch" }) }, + }; + } + + private void GivenNonEmptyMatchingRequestHeaders() + { + _requestHeaders = new HeaderDictionary() + { + { "header", new StringValues(new string[]{ "value" }) }, + }; + } + private void ThenAnErrorResponseIsReturned() { _result.IsError.ShouldBeTrue(); @@ -749,7 +907,7 @@ private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) private void WhenICallTheFinder() { - _result = _downstreamRouteFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost); + _result = _downstreamRouteFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _requestHeaders); } private void ThenTheFollowingIsReturned(DownstreamRoute expected)