Skip to content
Open
Show file tree
Hide file tree
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
49 changes: 48 additions & 1 deletion docs/features/routing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ e.g. you could have
"Priority": 0
}

and
and

.. code-block:: json

Expand Down Expand Up @@ -381,6 +381,53 @@ If needed, a more complex upstream header template can be specified using placeh

**Note 2**: Additionally, the ``UpstreamHeaderTemplates`` dictionary options are applicable for :doc:`../features/aggregation` as well.


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 Route with upstream headers, each of which may define a set of accepted values. If a Route 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 Route for a match.

A sample configuration might look like the following:

.. code-block:: json

{
"Routes": [
{
// Downstream* props
// Upstream* props
"UpstreamHeaderRoutingOptions": {
"Headers": {
"X-API-Version": [ "1" ],
"X-Tenant-Id": [ "tenantId" ]
},
"TriggerOn": "all"
}
},
{
// Downstream* props
// Upstream* props
"UpstreamHeaderRoutingOptions": {
"Headers": {
"X-API-Version": [ "1", "2" ]
},
"TriggerOn": "any"
}
}
]
}

The ``UpstreamHeaderRoutingOptions`` block defines two attributes: the ``Headers`` block and the ``TriggerOn`` 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, ``TriggerOn``, defines how the route finder will determine whether a particular header configuration in a request matches a Route's header configuration. The attribute accepts two values:

* ``"Any"`` causes the route finder to match a Route if any value of *any* configured header is present in a request
* ``"All"`` causes the route finder to match a Route only if any value of *all* configured headers is present in a request

.. _routing-security-options:

Security Options [#f6]_
Expand Down
10 changes: 9 additions & 1 deletion src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Ocelot.Configuration.Creator;
using Microsoft.AspNetCore.Routing;
using Ocelot.Configuration.Creator;
using Ocelot.Infrastructure.Extensions;
using Ocelot.Values;

Expand Down Expand Up @@ -40,6 +41,7 @@ public class DownstreamRouteBuilder
private Dictionary<string, UpstreamHeaderTemplate> _upstreamHeaders;
private MetadataOptions _metadataOptions;
private int? _timeout;
private UpstreamHeaderRoutingOptions _upstreamHeaderRoutingOptions;

public DownstreamRouteBuilder()
{
Expand Down Expand Up @@ -253,6 +255,12 @@ public DownstreamRouteBuilder WithTimeout(int? timeout)
return this;
}

public DownstreamRouteBuilder WithUpstreamHeaderRoutingOptions(UpstreamHeaderRoutingOptions routingOptions)
{
_upstreamHeaderRoutingOptions = routingOptions;
return this;
}

public DownstreamRoute Build()
{
return new DownstreamRoute(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Creator;

public interface IUpstreamHeaderRoutingOptionsCreator
{
UpstreamHeaderRoutingOptions Create(FileUpstreamHeaderRoutingOptions options);
}
9 changes: 8 additions & 1 deletion src/Ocelot/Configuration/Creator/StaticRoutesCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class StaticRoutesCreator : IRoutesCreator
private readonly IVersionCreator _versionCreator;
private readonly IVersionPolicyCreator _versionPolicyCreator;
private readonly IMetadataCreator _metadataCreator;
private readonly IUpstreamHeaderRoutingOptionsCreator _upstreamHeaderRoutingOptionsCreator;

public StaticRoutesCreator(
IClaimsToThingCreator claimsToThingCreator,
Expand All @@ -41,7 +42,8 @@ public StaticRoutesCreator(
IVersionCreator versionCreator,
IVersionPolicyCreator versionPolicyCreator,
IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator,
IMetadataCreator metadataCreator)
IMetadataCreator metadataCreator,
IUpstreamHeaderRoutingOptionsCreator upstreamHeaderRoutingOptionsCreator)
{
_routeKeyCreator = routeKeyCreator;
_loadBalancerOptionsCreator = loadBalancerOptionsCreator;
Expand All @@ -61,6 +63,7 @@ public StaticRoutesCreator(
_versionPolicyCreator = versionPolicyCreator;
_upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator;
_metadataCreator = metadataCreator;
_upstreamHeaderRoutingOptionsCreator = upstreamHeaderRoutingOptionsCreator;
}

public IReadOnlyList<Route> Create(FileConfiguration fileConfiguration)
Expand All @@ -83,6 +86,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf
var requestIdKey = _requestIdKeyCreator.Create(fileRoute, globalConfiguration);

var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute);
var upstreamHeaderRoutingOptions = _upstreamHeaderRoutingOptionsCreator.Create(fileRoute.UpstreamHeaderRoutingOptions);

var authOptions = _authOptionsCreator.Create(fileRoute, globalConfiguration);

Expand Down Expand Up @@ -151,6 +155,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf
.WithUpstreamHeaderFindAndReplace(hAndRs.Upstream)
.WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod.ToList())
.WithUpstreamPathTemplate(upstreamTemplatePattern)
.WithUpstreamHeaderRoutingOptions(upstreamHeaderRoutingOptions)
.Build();
return route;
}
Expand All @@ -160,12 +165,14 @@ private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoute)
var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); // TODO It should be downstreamRoute.UpstreamPathTemplate
var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute); // TODO It should be downstreamRoute.UpstreamHeaders
var upstreamHttpMethods = fileRoute.UpstreamHttpMethod.ToHttpMethods();
var upstreamHeaderRoutingOptions = _upstreamHeaderRoutingOptionsCreator.Create(fileRoute.UpstreamHeaderRoutingOptions);
return new Route(downstreamRoute)
{
UpstreamHeaderTemplates = upstreamHeaderTemplates, // downstreamRoute.UpstreamHeaders
UpstreamHost = fileRoute.UpstreamHost,
UpstreamHttpMethod = upstreamHttpMethods,
UpstreamTemplatePattern = upstreamTemplatePattern,
UpstreamHeaderRoutingOptions = upstreamHeaderRoutingOptions,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Creator;

public class UpstreamHeaderRoutingOptionsCreator : IUpstreamHeaderRoutingOptionsCreator
{
public UpstreamHeaderRoutingOptions Create(FileUpstreamHeaderRoutingOptions options)
{
var mode = UpstreamHeaderRoutingTriggerMode.Any;
if (options.TriggerOn.Length > 0)
{
mode = Enum.Parse<UpstreamHeaderRoutingTriggerMode>(options.TriggerOn, true);
}

// Keys are converted to uppercase as apparently that is the preferred
// approach according to https://learn.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings
// Values are left untouched but value comparison at runtime is done in
// a case-insensitive manner by using the appropriate StringComparer.
var headers = options.Headers.ToDictionary(
kv => kv.Key.ToUpperInvariant(),
kv => kv.Value);

return new UpstreamHeaderRoutingOptions(headers, mode);
}
}
9 changes: 6 additions & 3 deletions src/Ocelot/Configuration/File/FileRoute.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
namespace Ocelot.Configuration.File;
namespace Ocelot.Configuration.File;

/// <summary>
/// Represents the JSON structure of a standard static route (no service discovery).
/// </summary>
public class FileRoute : FileRouteBase, IRouteUpstream, IRouteGrouping, IRouteRateLimiting, ICloneable
{
{
public FileRoute()
{
AddClaimsToRequest = new Dictionary<string, string>();
Expand All @@ -19,6 +19,7 @@
SecurityOptions = new FileSecurityOptions();
UpstreamHeaderTemplates = new Dictionary<string, string>();
UpstreamHeaderTransform = new Dictionary<string, string>();
UpstreamHeaderRoutingOptions = new FileUpstreamHeaderRoutingOptions();
UpstreamHttpMethod = new();
}

Expand All @@ -44,6 +45,7 @@
public Dictionary<string, string> RouteClaimsRequirement { get; set; }
public bool RouteIsCaseSensitive { get; set; }
public FileSecurityOptions SecurityOptions { get; set; }
public FileUpstreamHeaderRoutingOptions UpstreamHeaderRoutingOptions { get; set; }
public IDictionary<string, string> UpstreamHeaderTemplates { get; set; }
public IDictionary<string, string> UpstreamHeaderTransform { get; set; }
public string UpstreamHost { get; set; }
Expand Down Expand Up @@ -78,7 +80,7 @@
to.DownstreamPathTemplate = from.DownstreamPathTemplate;
to.DownstreamScheme = from.DownstreamScheme;
to.CacheOptions = new(from.CacheOptions);
to.FileCacheOptions = new(from.FileCacheOptions);

Check warning on line 83 in src/Ocelot/Configuration/File/FileRoute.cs

View workflow job for this annotation

GitHub Actions / build-cake

'FileRoute.FileCacheOptions' is obsolete: 'Use CacheOptions instead of FileCacheOptions! Note that FileCacheOptions will be removed in version 25.0!'

Check warning on line 83 in src/Ocelot/Configuration/File/FileRoute.cs

View workflow job for this annotation

GitHub Actions / build-cake

'FileRoute.FileCacheOptions' is obsolete: 'Use CacheOptions instead of FileCacheOptions! Note that FileCacheOptions will be removed in version 25.0!'
to.HttpHandlerOptions = new(from.HttpHandlerOptions);
to.Key = from.Key;
to.LoadBalancerOptions = new(from.LoadBalancerOptions);
Expand All @@ -94,6 +96,7 @@
to.ServiceName = from.ServiceName;
to.ServiceNamespace = from.ServiceNamespace;
to.Timeout = from.Timeout;
to.UpstreamHeaderRoutingOptions = from.UpstreamHeaderRoutingOptions;
to.UpstreamHeaderTemplates = new Dictionary<string, string>(from.UpstreamHeaderTemplates);
to.UpstreamHeaderTransform = new Dictionary<string, string>(from.UpstreamHeaderTransform);
to.UpstreamHost = from.UpstreamHost;
Expand All @@ -114,5 +117,5 @@
return !string.IsNullOrWhiteSpace(ServiceName)
? string.Join(':', ServiceNamespace, ServiceName, path)
: path;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Ocelot.Configuration.File;

public class FileUpstreamHeaderRoutingOptions
{
public IDictionary<string, ICollection<string>> Headers { get; set; } = new Dictionary<string, ICollection<string>>();

public string TriggerOn { get; set; } = string.Empty;
}
1 change: 1 addition & 0 deletions src/Ocelot/Configuration/Route.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ public Route(DownstreamRoute route, HttpMethod method)
public string UpstreamHost { get; init; }
public HashSet<HttpMethod> UpstreamHttpMethod { get; init; }
public UpstreamPathTemplate UpstreamTemplatePattern { get; init; }
public UpstreamHeaderRoutingOptions UpstreamHeaderRoutingOptions { get; init; }
}
16 changes: 16 additions & 0 deletions src/Ocelot/Configuration/UpstreamHeaderRoutingOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Ocelot.Configuration;

public class UpstreamHeaderRoutingOptions
{
public UpstreamHeaderRoutingOptions(IReadOnlyDictionary<string, ICollection<string>> headers, UpstreamHeaderRoutingTriggerMode mode)
{
Headers = new UpstreamRoutingHeaders(headers);
Mode = mode;
}

public bool Enabled() => Headers.Any();

public UpstreamRoutingHeaders Headers { get; }

public UpstreamHeaderRoutingTriggerMode Mode { get; }
}
7 changes: 7 additions & 0 deletions src/Ocelot/Configuration/UpstreamHeaderRoutingTriggerMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Ocelot.Configuration;

public enum UpstreamHeaderRoutingTriggerMode : byte
{
Any,
All,
}
62 changes: 62 additions & 0 deletions src/Ocelot/Configuration/UpstreamRoutingHeaders.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace Ocelot.Configuration;

public class UpstreamRoutingHeaders
{
public IReadOnlyDictionary<string, ICollection<string>> Headers { get; }

public UpstreamRoutingHeaders(IReadOnlyDictionary<string, ICollection<string>> headers)
{
Headers = headers;
}

public bool Any() => Headers.Any();

public bool HasAnyOf(IHeaderDictionary requestHeaders)
{
IHeaderDictionary normalizedHeaders = NormalizeHeaderNames(requestHeaders);
foreach (var h in Headers)
{
if (normalizedHeaders.TryGetValue(h.Key, out var values) &&
h.Value.Intersect(values, StringComparer.OrdinalIgnoreCase).Any())
{
return true;
}
}

return false;
}

public bool HasAllOf(IHeaderDictionary requestHeaders)
{
IHeaderDictionary normalizedHeaders = NormalizeHeaderNames(requestHeaders);
foreach (var h in Headers)
{
if (!normalizedHeaders.TryGetValue(h.Key, out var values))
{
return false;
}

if (!h.Value.Intersect(values, StringComparer.OrdinalIgnoreCase).Any())
{
return false;
}
}

return true;
}

private static IHeaderDictionary NormalizeHeaderNames(IHeaderDictionary headers)
{
var upperCaseHeaders = new HeaderDictionary();
foreach (KeyValuePair<string, StringValues> kv in headers)
{
var key = kv.Key.ToUpperInvariant();
upperCaseHeaders.Add(key, kv.Value);
}

return upperCaseHeaders;
}
}
Loading
Loading