Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upstream header routing #964

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 49 additions & 16 deletions docs/features/routing.rst
Original file line number Diff line number Diff line change
@@ -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 <https://github.com/ThreeMammals/Ocelot/pull/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 <https://github.com/ThreeMammals/Ocelot/issues/340>`_.
This feature was requested in `issue 340 <https://github.com/ThreeMammals/Ocelot/issues/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 <https://github.com/ThreeMammals/Ocelot/issues/360>`_ and `issue 624 <https://github.com/ThreeMammals/Ocelot/issues/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.
29 changes: 18 additions & 11 deletions src/Ocelot/Configuration/Builder/ReRouteBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<DownstreamReRoute> _downstreamReRoutes;
private List<AggregateReRouteConfig> _downstreamReRoutesConfig;
private string _aggregator;
private UpstreamHeaderRoutingOptions _upstreamHeaderRoutingOptions;

public ReRouteBuilder()
{
@@ -55,24 +56,30 @@ public ReRouteBuilder WithAggregateReRouteConfig(List<AggregateReRouteConfig> 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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Creator
{
public interface IUpstreamHeaderRoutingOptionsCreator
{
UpstreamHeaderRoutingOptions Create(FileUpstreamHeaderRoutingOptions options);
}
}
8 changes: 7 additions & 1 deletion src/Ocelot/Configuration/Creator/ReRoutesCreator.cs
Original file line number Diff line number Diff line change
@@ -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<ReRoute> 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;
Original file line number Diff line number Diff line change
@@ -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<string, HashSet<string>> headers = options.Headers.ToDictionary(
kv => kv.Key.ToLowerInvariant(),
kv => new HashSet<string>(kv.Value.Select(v => v.ToLowerInvariant())));

return new UpstreamHeaderRoutingOptions(headers, mode);
}
}
}
2 changes: 2 additions & 0 deletions src/Ocelot/Configuration/File/FileReRoute.cs
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ public FileReRoute()
DelegatingHandlers = new List<string>();
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; }
}
}
17 changes: 17 additions & 0 deletions src/Ocelot/Configuration/File/FileUpstreamHeaderRoutingOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Collections.Generic;

namespace Ocelot.Configuration.File
{
public class FileUpstreamHeaderRoutingOptions
{
public FileUpstreamHeaderRoutingOptions()
{
Headers = new Dictionary<string, List<string>>();
CombinationMode = "";
}

public Dictionary<string, List<string>> Headers { get; set; }

public string CombinationMode { get; set; }
}
}
11 changes: 7 additions & 4 deletions src/Ocelot/Configuration/ReRoute.cs
Original file line number Diff line number Diff line change
@@ -9,17 +9,19 @@ public class ReRoute
{
public ReRoute(List<DownstreamReRoute> downstreamReRoute,
List<AggregateReRouteConfig> downstreamReRouteConfig,
List<HttpMethod> upstreamHttpMethod,
UpstreamPathTemplate upstreamTemplatePattern,
List<HttpMethod> upstreamHttpMethod,
UpstreamPathTemplate upstreamTemplatePattern,
string upstreamHost,
string aggregator)
string aggregator,
UpstreamHeaderRoutingOptions upstreamHeaderRoutingOptions)
{
UpstreamHost = upstreamHost;
DownstreamReRoute = downstreamReRoute;
DownstreamReRouteConfig = downstreamReRouteConfig;
UpstreamHttpMethod = upstreamHttpMethod;
UpstreamTemplatePattern = upstreamTemplatePattern;
Aggregator = aggregator;
UpstreamHeaderRoutingOptions = upstreamHeaderRoutingOptions;
}

public UpstreamPathTemplate UpstreamTemplatePattern { get; private set; }
@@ -28,5 +30,6 @@ public ReRoute(List<DownstreamReRoute> downstreamReRoute,
public List<DownstreamReRoute> DownstreamReRoute { get; private set; }
public List<AggregateReRouteConfig> DownstreamReRouteConfig { get; private set; }
public string Aggregator { get; private set; }
public UpstreamHeaderRoutingOptions UpstreamHeaderRoutingOptions { get; private set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Ocelot.Configuration
{
public enum UpstreamHeaderRoutingCombinationMode
{
Any = 0,
All = 1,
}
}
19 changes: 19 additions & 0 deletions src/Ocelot/Configuration/UpstreamHeaderRoutingOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;

namespace Ocelot.Configuration
{
public class UpstreamHeaderRoutingOptions
{
public UpstreamHeaderRoutingOptions(Dictionary<string, HashSet<string>> 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; }
}
}
70 changes: 70 additions & 0 deletions src/Ocelot/Configuration/UpstreamRoutingHeaders.cs
Original file line number Diff line number Diff line change
@@ -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<string, HashSet<string>> headers)
{
Headers = headers;
}

public bool Empty() => Headers.Count == 0;

public bool HasAnyOf(IHeaderDictionary requestHeaders)
{
IHeaderDictionary lowerCaseHeaders = GetLowerCaseHeaders(requestHeaders);
foreach (KeyValuePair<string, HashSet<string>> h in Headers)
{
if (lowerCaseHeaders.TryGetValue(h.Key, out var values))
{
HashSet<string> requestHeaderValues = new HashSet<string>(values);
if (h.Value.Overlaps(requestHeaderValues))
{
return true;
}
}
}

return false;
}

public bool HasAllOf(IHeaderDictionary requestHeaders)
{
IHeaderDictionary lowerCaseHeaders = GetLowerCaseHeaders(requestHeaders);
foreach (KeyValuePair<string, HashSet<string>> h in Headers)
{
if (!lowerCaseHeaders.TryGetValue(h.Key, out var values))
{
return false;
}

HashSet<string> requestHeaderValues = new HashSet<string>(values);
if (!h.Value.Overlaps(requestHeaderValues))
{
return false;
}
}

return true;
}

private IHeaderDictionary GetLowerCaseHeaders(IHeaderDictionary headers)
{
IHeaderDictionary lowerCaseHeaders = new HeaderDictionary();
foreach (KeyValuePair<string, StringValues> 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<string, HashSet<string>> Headers { get; private set; }
}
}
1 change: 1 addition & 0 deletions src/Ocelot/DependencyInjection/OcelotBuilder.cs
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo
Services.TryAddSingleton<IClaimsToThingCreator, ClaimsToThingCreator>();
Services.TryAddSingleton<IAuthenticationOptionsCreator, AuthenticationOptionsCreator>();
Services.TryAddSingleton<IUpstreamTemplatePatternCreator, UpstreamTemplatePatternCreator>();
Services.TryAddSingleton<IUpstreamHeaderRoutingOptionsCreator, UpstreamHeaderRoutingOptionsCreator>();
Services.TryAddSingleton<IRequestIdKeyCreator, RequestIdKeyCreator>();
Services.TryAddSingleton<IServiceProviderConfigurationCreator, ServiceProviderConfigurationCreator>();
Services.TryAddSingleton<IQoSOptionsCreator, QoSOptionsCreator>();
Original file line number Diff line number Diff line change
@@ -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<string, OkResponse<DownstreamRoute>>();
}

public Response<DownstreamRoute> Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost)
public Response<DownstreamRoute> Get(
string upstreamUrlPath,
string upstreamQueryString,
string upstreamHttpMethod,
IInternalConfiguration configuration,
string upstreamHost,
IHeaderDictionary requestHeaders)
{
var serviceName = GetServiceName(upstreamUrlPath);

39 changes: 34 additions & 5 deletions src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs
Original file line number Diff line number Diff line change
@@ -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<DownstreamRoute> Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod, IInternalConfiguration configuration, string upstreamHost)
public Response<DownstreamRoute> Get(
string upstreamUrlPath,
string upstreamQueryString,
string httpMethod,
IInternalConfiguration configuration,
string upstreamHost,
IHeaderDictionary requestHeaders)
{
var downstreamRoutes = new List<DownstreamRoute>();

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<DownstreamRoute> Get(string upstreamUrlPath, string upstreamQuer
return new ErrorResponse<DownstreamRoute>(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)
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
using Ocelot.Configuration;
using Ocelot.Responses;
using Microsoft.AspNetCore.Http;

namespace Ocelot.DownstreamRouteFinder.Finder
{
public interface IDownstreamRouteProvider
{
Response<DownstreamRoute> Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost);
Response<DownstreamRoute> Get(
string upstreamUrlPath,
string upstreamQueryString,
string upstreamHttpMethod,
IInternalConfiguration configuration,
string upstreamHost,
IHeaderDictionary requestHeaders);
}
}
Original file line number Diff line number Diff line change
@@ -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)
{
6 changes: 5 additions & 1 deletion test/Ocelot.UnitTests/Configuration/ReRoutesCreatorTests.cs
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@ public class ReRoutesCreatorTests
private Mock<ILoadBalancerOptionsCreator> _lboCreator;
private Mock<IReRouteKeyCreator> _rrkCreator;
private Mock<ISecurityOptionsCreator> _soCreator;
private Mock<IUpstreamHeaderRoutingOptionsCreator> _uhroCreator;
private FileConfiguration _fileConfig;
private ReRouteOptions _rro;
private string _requestId;
@@ -46,6 +47,7 @@ public class ReRoutesCreatorTests
private LoadBalancerOptions _lbo;
private List<ReRoute> _result;
private SecurityOptions _securityOptions;
private UpstreamHeaderRoutingOptions _upstreamHeaderRoutingOptions;

public ReRoutesCreatorTests()
{
@@ -63,6 +65,7 @@ public ReRoutesCreatorTests()
_lboCreator = new Mock<ILoadBalancerOptionsCreator>();
_rrkCreator = new Mock<IReRouteKeyCreator>();
_soCreator = new Mock<ISecurityOptionsCreator>();
_uhroCreator = new Mock<IUpstreamHeaderRoutingOptionsCreator>();

_creator = new ReRoutesCreator(
_cthCreator.Object,
@@ -78,7 +81,8 @@ public ReRoutesCreatorTests()
_daCreator.Object,
_lboCreator.Object,
_rrkCreator.Object,
_soCreator.Object
_soCreator.Object,
_uhroCreator.Object
);
}

Original file line number Diff line number Diff line change
@@ -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<string, HashSet<string>>()
{
{ "header1", new HashSet<string>() { "value1", "value2" }},
{ "header2", new HashSet<string>() { "value3" }},
},
mode: UpstreamHeaderRoutingCombinationMode.All
);

this.Given(_ => GivenTheseFileUpstreamHeaderRoutingOptions())
.When(_ => WhenICreate())
.Then(_ => ThenTheCreatedMatchesThis(expected))
.BDDfy();
}

private void GivenTheseFileUpstreamHeaderRoutingOptions()
{
_fileUpstreamHeaderRoutingOptions = new FileUpstreamHeaderRoutingOptions()
{
Headers = new Dictionary<string, List<string>>()
{
{ "Header1", new List<string>() { "Value1", "Value2" }},
{ "Header2", new List<string>() { "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<string, HashSet<string>> 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);
}
}
}
143 changes: 143 additions & 0 deletions test/Ocelot.UnitTests/Configuration/UpstreamRoutingHeadersTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, HashSet<string>> _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<string, HashSet<string>>();
}

private void GivenPresetHeaderDictionary()
{
_headersDictionary = new Dictionary<string, HashSet<string>>()
{
{ "testheader1", new HashSet<string>() { "testheader1value1", "testheader1value2" } },
{ "testheader2", new HashSet<string>() { "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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<IQoSOptionsCreator> _qosOptionsCreator;
private Response<DownstreamRoute> _resultTwo;
private string _upstreamQuery;
private readonly IHeaderDictionary _upstreamHeaders;

public DownstreamRouteCreatorTests()
{
@@ -39,6 +41,7 @@ public DownstreamRouteCreatorTests()
.Setup(x => x.Create(It.IsAny<QoSOptions>(), It.IsAny<string>(), It.IsAny<List<string>>()))
.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()
Original file line number Diff line number Diff line change
@@ -83,7 +83,7 @@ private void GivenTheDownStreamRouteFinderReturns(DownstreamRoute downstreamRout
{
_downstreamRoute = new OkResponse<DownstreamRoute>(downstreamRoute);
_finder
.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IInternalConfiguration>(), It.IsAny<string>()))
.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IInternalConfiguration>(), It.IsAny<string>(), It.IsAny<HeaderDictionary>()))
.Returns(_downstreamRoute);
}

Large diffs are not rendered by default.