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

#360 Routing based on values in designated upstream request headers #1684

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
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 @@ -178,7 +178,7 @@ e.g. you could have
"Priority": 0
}

and
and

.. code-block:: json

Expand Down Expand Up @@ -292,6 +292,53 @@ Here are two user scenarios.
So, both ``{userId}`` placeholder and ``userId`` parameter **names are the same**!
Finally, the ``userId`` parameter is removed.


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 [#f3]_
Expand Down
14 changes: 11 additions & 3 deletions src/Ocelot/Configuration/Builder/RouteBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Ocelot.Configuration.File;
using Ocelot.Values;
using Ocelot.Configuration.File;
using Ocelot.Values;

namespace Ocelot.Configuration.Builder
{
Expand All @@ -11,6 +11,7 @@ public class RouteBuilder
private List<DownstreamRoute> _downstreamRoutes;
private List<AggregateRouteConfig> _downstreamRoutesConfig;
private string _aggregator;
private UpstreamHeaderRoutingOptions _upstreamHeaderRoutingOptions;

public RouteBuilder()
{
Expand Down Expand Up @@ -60,6 +61,12 @@ public RouteBuilder WithAggregator(string aggregator)
return this;
}

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

public Route Build()
{
return new Route(
Expand All @@ -68,7 +75,8 @@ public Route Build()
_upstreamHttpMethod,
_upstreamTemplatePattern,
_upstreamHost,
_aggregator
_aggregator,
_upstreamHeaderRoutingOptions
);
}
}
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);
}
11 changes: 8 additions & 3 deletions src/Ocelot/Configuration/Creator/RoutesCreator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Ocelot.Cache;
using Ocelot.Configuration.Builder;
using Ocelot.Cache;
using Ocelot.Configuration.Builder;
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Creator
Expand All @@ -21,6 +21,7 @@ public class RoutesCreator : IRoutesCreator
private readonly IRouteKeyCreator _routeKeyCreator;
private readonly ISecurityOptionsCreator _securityOptionsCreator;
private readonly IVersionCreator _versionCreator;
private readonly IUpstreamHeaderRoutingOptionsCreator _upstreamHeaderRoutingOptionsCreator;

public RoutesCreator(
IClaimsToThingCreator claimsToThingCreator,
Expand All @@ -37,7 +38,8 @@ public RoutesCreator(
ILoadBalancerOptionsCreator loadBalancerOptionsCreator,
IRouteKeyCreator routeKeyCreator,
ISecurityOptionsCreator securityOptionsCreator,
IVersionCreator versionCreator
IVersionCreator versionCreator,
IUpstreamHeaderRoutingOptionsCreator upstreamHeaderRoutingOptionsCreator
)
{
_routeKeyCreator = routeKeyCreator;
Expand All @@ -56,6 +58,7 @@ IVersionCreator versionCreator
_loadBalancerOptionsCreator = loadBalancerOptionsCreator;
_securityOptionsCreator = securityOptionsCreator;
_versionCreator = versionCreator;
_upstreamHeaderRoutingOptionsCreator = upstreamHeaderRoutingOptionsCreator;
}

public List<Route> Create(FileConfiguration fileConfiguration)
Expand Down Expand Up @@ -151,12 +154,14 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf
private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoutes)
{
var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute);
var upstreamHeaderRoutingOptions = _upstreamHeaderRoutingOptionsCreator.Create(fileRoute.UpstreamHeaderRoutingOptions);

var route = new RouteBuilder()
.WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod)
.WithUpstreamPathTemplate(upstreamTemplatePattern)
.WithDownstreamRoute(downstreamRoutes)
.WithUpstreamHost(fileRoute.UpstreamHost)
.WithUpstreamHeaderRoutingOptions(upstreamHeaderRoutingOptions)
.Build();

return route;
Expand Down
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);
}
}
25 changes: 14 additions & 11 deletions src/Ocelot/Configuration/File/FileRoute.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Ocelot.Configuration.File
namespace Ocelot.Configuration.File
{
public class FileRoute : IRoute, ICloneable
{
Expand All @@ -21,19 +21,20 @@ public FileRoute()
RouteClaimsRequirement = new Dictionary<string, string>();
SecurityOptions = new FileSecurityOptions();
UpstreamHeaderTransform = new Dictionary<string, string>();
UpstreamHeaderRoutingOptions = new FileUpstreamHeaderRoutingOptions();
UpstreamHttpMethod = new List<string>();
}

}
public FileRoute(FileRoute from)
{
DeepCopy(from, this);
}

public Dictionary<string, string> AddClaimsToRequest { get; set; }
public Dictionary<string, string> AddClaimsToRequest { get; set; }
public Dictionary<string, string> AddHeadersToRequest { get; set; }
public Dictionary<string, string> AddQueriesToRequest { get; set; }
public Dictionary<string, string> AddQueriesToRequest { get; set; }
public FileAuthenticationOptions AuthenticationOptions { get; set; }
public Dictionary<string, string> ChangeDownstreamPathTemplate { get; set; }
public Dictionary<string, string> ChangeDownstreamPathTemplate { get; set; }
public bool DangerousAcceptAnyServerCertificateValidator { get; set; }
public List<string> DelegatingHandlers { get; set; }
public Dictionary<string, string> DownstreamHeaderTransform { get; set; }
Expand All @@ -42,7 +43,7 @@ public FileRoute(FileRoute from)
public string DownstreamHttpVersion { get; set; }
public string DownstreamPathTemplate { get; set; }
public string DownstreamScheme { get; set; }
public FileCacheOptions FileCacheOptions { get; set; }
public FileCacheOptions FileCacheOptions { get; set; }
public FileHttpHandlerOptions HttpHandlerOptions { get; set; }
public string Key { get; set; }
public FileLoadBalancerOptions LoadBalancerOptions { get; set; }
Expand All @@ -51,13 +52,14 @@ public FileRoute(FileRoute from)
public FileRateLimitRule RateLimitOptions { get; set; }
public string RequestIdKey { get; set; }
public Dictionary<string, string> RouteClaimsRequirement { get; set; }
public bool RouteIsCaseSensitive { get; set; }
public bool RouteIsCaseSensitive { get; set; }
public FileSecurityOptions SecurityOptions { get; set; }
public string ServiceName { get; set; }
public string ServiceNamespace { get; set; }
public string ServiceName { get; set; }
public string ServiceNamespace { get; set; }
public int Timeout { get; set; }
public Dictionary<string, string> UpstreamHeaderTransform { get; set; }
public string UpstreamHost { get; set; }
public FileUpstreamHeaderRoutingOptions UpstreamHeaderRoutingOptions { get; set; }
public string UpstreamHost { get; set; }
public List<string> UpstreamHttpMethod { get; set; }
public string UpstreamPathTemplate { get; set; }

Expand Down Expand Up @@ -102,6 +104,7 @@ public static void DeepCopy(FileRoute from, FileRoute to)
to.ServiceNamespace = from.ServiceNamespace;
to.Timeout = from.Timeout;
to.UpstreamHeaderTransform = new(from.UpstreamHeaderTransform);
to.UpstreamHeaderRoutingOptions = from.UpstreamHeaderRoutingOptions;
to.UpstreamHost = from.UpstreamHost;
to.UpstreamHttpMethod = new(from.UpstreamHttpMethod);
to.UpstreamPathTemplate = from.UpstreamPathTemplate;
Expand Down
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;
}
5 changes: 4 additions & 1 deletion src/Ocelot/Configuration/Route.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ public Route(List<DownstreamRoute> downstreamRoute,
List<HttpMethod> upstreamHttpMethod,
UpstreamPathTemplate upstreamTemplatePattern,
string upstreamHost,
string aggregator)
string aggregator,
UpstreamHeaderRoutingOptions upstreamHeaderRoutingOptions)
{
UpstreamHost = upstreamHost;
DownstreamRoute = downstreamRoute;
DownstreamRouteConfig = downstreamRouteConfig;
UpstreamHttpMethod = upstreamHttpMethod;
UpstreamTemplatePattern = upstreamTemplatePattern;
Aggregator = aggregator;
UpstreamHeaderRoutingOptions = upstreamHeaderRoutingOptions;
}

public UpstreamPathTemplate UpstreamTemplatePattern { get; }
Expand All @@ -26,5 +28,6 @@ public Route(List<DownstreamRoute> downstreamRoute,
public List<DownstreamRoute> DownstreamRoute { get; }
public List<AggregateRouteConfig> DownstreamRouteConfig { get; }
public string Aggregator { get; }
public UpstreamHeaderRoutingOptions UpstreamHeaderRoutingOptions { get; }
}
}
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; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public IReadOnlyDictionary<string, ICollection<string>> Headers { get; }
public readonly IReadOnlyDictionary<string, ICollection<string>> Headers { get; }

could be private ?


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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach (var h in Headers)
foreach (var header in Headers)

{
if (normalizedHeaders.TryGetValue(h.Key, out var values) &&
h.Value.Intersect(values, StringComparer.OrdinalIgnoreCase).Any())
{
return true;
}
}
Comment on lines +19 to +27
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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;
}
}
var normalizedHeaders = NormalizeHeaderNames(requestHeaders);
foreach (var (key, value) in Headers)
{
if (normalizedHeaders.TryGetValue(key, out var values))
{
if (value.Any(item => values.Contains(item, StringComparer.OrdinalIgnoreCase)))
{
return true;
}
}
}


return false;
}

public bool HasAllOf(IHeaderDictionary requestHeaders)
{
IHeaderDictionary normalizedHeaders = NormalizeHeaderNames(requestHeaders);
foreach (var h in Headers)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach (var h in Headers)
foreach (var header 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;
}
Comment on lines +51 to +61
Copy link
Collaborator

@RaynaldM RaynaldM Feb 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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;
}
private static IHeaderDictionary NormalizeHeaderNames(IHeaderDictionary headers) =>
new HeaderDictionary(headers.ToDictionary(
h => h.Key.ToUpperInvariant(),
h => h.Value
));

}
Loading