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 request header #1312

Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
36a1e28
routing based on headers (all specified headers must match)
jlukawska Jul 7, 2020
3630d50
routing based on headers for aggregated routes
jlukawska Jul 9, 2020
c392809
unit tests and small modifications
jlukawska Jul 10, 2020
1b74d53
find placeholders in header templates
jlukawska Jul 15, 2020
e121c8a
match upstream headers to header templates
jlukawska Jul 15, 2020
073c6fe
find placeholders name and values, fix regex for finding placeholders…
jlukawska Jul 16, 2020
800a025
fix unit tests
jlukawska Jul 23, 2020
a72a3d9
change header placeholder pattern
jlukawska Jul 24, 2020
c2829e7
unit tests
jlukawska Jul 28, 2020
e14ce8e
unit tests
jlukawska Jul 28, 2020
4ad836a
unit tests
jlukawska Jul 29, 2020
565e1c2
unit tests
jlukawska Aug 3, 2020
d797679
extend validation with checking upstreamheadertemplates, acceptance t…
jlukawska Aug 5, 2020
8c9cd40
update docs and minor changes
jlukawska Aug 13, 2020
cac4c3b
SA1649 File name should match first type name
raman-m Aug 2, 2023
d2b9cd7
Fix compilation errors by code review after resolving conflicts
raman-m Aug 2, 2023
30f5b3d
Fix warnings
raman-m Aug 2, 2023
2a32d1f
File-scoped namespaces
raman-m Aug 2, 2023
dec3145
File-scoped namespace
raman-m Aug 2, 2023
e13f19a
Target-typed 'new' expressions (C# 9).
raman-m Aug 2, 2023
39edcaa
IDE1006 Naming rule violation: These words must begin with upper case…
raman-m Aug 2, 2023
2dcb983
Target-typed 'new' expressions (C# 9).
raman-m Aug 2, 2023
d6c8659
Fix build errors
raman-m Apr 2, 2024
29d58b7
DownstreamRouteBuilder
raman-m Apr 6, 2024
8a64955
AggregatesCreator
raman-m Apr 6, 2024
864b63a
IUpstreamHeaderTemplatePatternCreator, RoutesCreator
raman-m Apr 6, 2024
79adba6
UpstreamHeaderTemplatePatternCreator
raman-m Apr 6, 2024
9a1d117
FileAggregateRoute
raman-m Apr 6, 2024
7b2dd43
FileAggregateRoute
raman-m Apr 6, 2024
0bf166e
FileRoute
raman-m Apr 6, 2024
a9f3f0e
Route, IRoute
raman-m Apr 6, 2024
790d590
FileConfigurationFluentValidator
raman-m Apr 6, 2024
38cd00c
OcelotBuilder
raman-m Apr 6, 2024
0667a93
DownstreamRouteCreator
raman-m Apr 6, 2024
079f9d2
DownstreamRouteFinder
raman-m Apr 6, 2024
32d8310
HeaderMatcher
raman-m Apr 6, 2024
36cf99f
DownstreamRouteFinderMiddleware
raman-m Apr 6, 2024
98fe814
UpstreamHeaderTemplate
raman-m Apr 6, 2024
ea55272
Routing folder
raman-m Apr 6, 2024
16f6f88
RoutingBasedOnHeadersTests
raman-m Apr 6, 2024
7f5bb67
Refactor acceptance tests
raman-m Apr 6, 2024
6e7ab00
AAA pattern in unit tests
raman-m Apr 12, 2024
6aeef96
CS8936: Feature 'collection expressions' is not available in C# 10.0.
raman-m Apr 12, 2024
c727b93
Code review by @RaynaldM
raman-m Apr 15, 2024
7e98824
Convert facts to one `Theory`
raman-m Apr 15, 2024
6a6a417
AAA pattern
raman-m Apr 15, 2024
75c9d85
Add traits
raman-m Apr 15, 2024
57e2fdb
Update routing.rst
raman-m Apr 15, 2024
dc5a671
Update docs
raman-m Apr 15, 2024
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
1 change: 1 addition & 0 deletions docs/features/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Here is an example Route configuration. You don't need to set all of these thing
"UpstreamHttpMethod": [ "Get" ],
"DownstreamHttpMethod": "",
"DownstreamHttpVersion": "",
"UpstreamHeaderTemplates": {},
"AddHeadersToRequest": {},
"AddClaimsToRequest": {},
"RouteClaimsRequirement": {},
Expand Down
56 changes: 56 additions & 0 deletions docs/features/routing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,62 @@ The Route above will only be matched when the ``Host`` header value is ``somedom
If you do not set **UpstreamHost** on a Route then any ``Host`` header will match it.
This means that if you have two Routes 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.

Upstream Headers
^^^^^^^^^^^^^^^^

Additionally to routing by UpstreamPathTemplate, you can define UpstreamHeaderTemplates. Then to match a route, all the headers from this section must exist in the request headers.

The example:

.. code-block:: json

{
"DownstreamPathTemplate": "/",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "10.0.10.1",
"Port": 80,
}
],
"UpstreamPathTemplate": "/",
"UpstreamHttpMethod": [ "Get" ],
"UpstreamHeaderTemplates": {
"country": "uk",
"version": "v1"
}
}

In this case the route will be matched only if a request has both headers set to the configured values.

You can use placeholders in your UpstreamHeaderTemplates in the following way:

.. code-block:: json

{
"DownstreamPathTemplate": "/{versionnumber}/api",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "10.0.10.1",
"Port": 80,
}
],
"UpstreamPathTemplate": "/api",
"UpstreamHttpMethod": [ "Get" ],
"UpstreamHeaderTemplates": {
"version": "{header:versionnumber}"
}
}

In this case the whole value for the request header "version" will be placed into the DownstreamPathTemplate. If you need, you can specify more complex upstream header template with placeholders like e.g. "version-{header:version}_country-{header:country}".

Placeholders do not have to exist in DownstreamPathTemplate. This case may be used to require specific header no matter what the value it contains.

UpstreamHeaderTemplates section can be used also for Aggregates.

This feature was requested in `Issue 360 <https://github.com/ThreeMammals/Ocelot/pull/360>`_ .

Priority
--------

Expand Down
22 changes: 16 additions & 6 deletions src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@ public class DownstreamRouteBuilder
private SecurityOptions _securityOptions;
private string _downstreamHttpMethod;
private Version _downstreamHttpVersion;
private Dictionary<string, UpstreamHeaderTemplate> _upstreamHeaders;

public DownstreamRouteBuilder()
{
_downstreamAddresses = new List<DownstreamHostAndPort>();
_delegatingHandlers = new List<string>();
_addHeadersToDownstream = new List<AddHeader>();
_addHeadersToUpstream = new List<AddHeader>();
_downstreamAddresses = new();
_delegatingHandlers = new();
_addHeadersToDownstream = new();
_addHeadersToUpstream = new();
}

public DownstreamRouteBuilder WithDownstreamAddresses(List<DownstreamHostAndPort> downstreamAddresses)
Expand Down Expand Up @@ -87,7 +88,9 @@ public DownstreamRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate inpu

public DownstreamRouteBuilder WithUpstreamHttpMethod(List<string> input)
{
_upstreamHttpMethod = (input.Count == 0) ? new List<HttpMethod>() : input.Select(x => new HttpMethod(x.Trim())).ToList();
_upstreamHttpMethod = input.Count > 0
? input.Select(x => new HttpMethod(x.Trim())).ToList()
: new();
return this;
}

Expand Down Expand Up @@ -259,6 +262,12 @@ public DownstreamRouteBuilder WithDownstreamHttpVersion(Version downstreamHttpVe
return this;
}

public DownstreamRouteBuilder WithUpstreamHeaders(Dictionary<string, UpstreamHeaderTemplate> input)
{
_upstreamHeaders = input;
return this;
}

public DownstreamRoute Build()
{
return new DownstreamRoute(
Expand Down Expand Up @@ -295,6 +304,7 @@ public DownstreamRoute Build()
_dangerousAcceptAnyServerCertificateValidator,
_securityOptions,
_downstreamHttpMethod,
_downstreamHttpVersion);
_downstreamHttpVersion,
_upstreamHeaders);
}
}
12 changes: 10 additions & 2 deletions src/Ocelot/Configuration/Builder/RouteBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public class RouteBuilder
private string _upstreamHost;
private List<DownstreamRoute> _downstreamRoutes;
private List<AggregateRouteConfig> _downstreamRoutesConfig;
private string _aggregator;
private string _aggregator;
private Dictionary<string, UpstreamHeaderTemplate> _upstreamHeaders;
raman-m marked this conversation as resolved.
Show resolved Hide resolved

public RouteBuilder()
{
Expand Down Expand Up @@ -58,6 +59,12 @@ public RouteBuilder WithAggregator(string aggregator)
{
_aggregator = aggregator;
return this;
}

public RouteBuilder WithUpstreamHeaders(Dictionary<string, UpstreamHeaderTemplate> upstreamHeaders)
raman-m marked this conversation as resolved.
Show resolved Hide resolved
{
_upstreamHeaders = upstreamHeaders;
return this;
}

public Route Build()
Expand All @@ -68,7 +75,8 @@ public Route Build()
_upstreamHttpMethod,
_upstreamTemplatePattern,
_upstreamHost,
_aggregator
_aggregator,
_upstreamHeaders
);
}
}
Expand Down
14 changes: 9 additions & 5 deletions src/Ocelot/Configuration/Creator/AggregatesCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ namespace Ocelot.Configuration.Creator
{
public class AggregatesCreator : IAggregatesCreator
{
private readonly IUpstreamTemplatePatternCreator _creator;
private readonly IUpstreamTemplatePatternCreator _creator;
private readonly IUpstreamHeaderTemplatePatternCreator _headerCreator;

public AggregatesCreator(IUpstreamTemplatePatternCreator creator)
public AggregatesCreator(IUpstreamTemplatePatternCreator creator, IUpstreamHeaderTemplatePatternCreator headerCreator)
{
_creator = creator;
_creator = creator;
_headerCreator = headerCreator;
}

public List<Route> Create(FileConfiguration fileConfiguration, List<Route> routes)
Expand All @@ -35,15 +37,17 @@ private Route SetUpAggregateRoute(IEnumerable<Route> routes, FileAggregateRoute
applicableRoutes.Add(downstreamRoute);
}

var upstreamTemplatePattern = _creator.Create(aggregateRoute);
var upstreamTemplatePattern = _creator.Create(aggregateRoute);
var upstreamHeaderTemplates = _headerCreator.Create(aggregateRoute);

var route = new RouteBuilder()
.WithUpstreamHttpMethod(aggregateRoute.UpstreamHttpMethod)
.WithUpstreamPathTemplate(upstreamTemplatePattern)
.WithDownstreamRoutes(applicableRoutes)
.WithAggregateRouteConfig(aggregateRoute.RouteKeysConfig)
.WithUpstreamHost(aggregateRoute.UpstreamHost)
.WithAggregator(aggregateRoute.Aggregator)
.WithAggregator(aggregateRoute.Aggregator)
.WithUpstreamHeaders(upstreamHeaderTemplates)
.Build();

return route;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Ocelot.Configuration.File;
using Ocelot.Values;

namespace Ocelot.Configuration.Creator;

/// <summary>
/// Ocelot feature: <see href="https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/routing.rst#upstream-headers">Routing based on request header</see>.
/// </summary>
public interface IUpstreamHeaderTemplatePatternCreator
{
/// <summary>
/// Creates upstream templates based on route headers.
/// </summary>
/// <param name="route">The route info.</param>
/// <returns>A <see cref="Dictionary{TKey, TValue}"/> object where TKey is <see langword="string"/>, TValue is <see cref="UpstreamHeaderTemplate"/>.</returns>
Dictionary<string, UpstreamHeaderTemplate> Create(IRoute route);
raman-m marked this conversation as resolved.
Show resolved Hide resolved
}
14 changes: 9 additions & 5 deletions src/Ocelot/Configuration/Creator/RoutesCreator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Ocelot.Cache;
using Ocelot.Configuration.Builder;
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Creator
{
public class RoutesCreator : IRoutesCreator
Expand All @@ -10,6 +10,7 @@ public class RoutesCreator : IRoutesCreator
private readonly IClaimsToThingCreator _claimsToThingCreator;
private readonly IAuthenticationOptionsCreator _authOptionsCreator;
private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator;
private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator;
private readonly IRequestIdKeyCreator _requestIdKeyCreator;
private readonly IQoSOptionsCreator _qosOptionsCreator;
private readonly IRouteOptionsCreator _fileRouteOptionsCreator;
Expand Down Expand Up @@ -37,8 +38,8 @@ public RoutesCreator(
ILoadBalancerOptionsCreator loadBalancerOptionsCreator,
IRouteKeyCreator routeKeyCreator,
ISecurityOptionsCreator securityOptionsCreator,
IVersionCreator versionCreator
)
IVersionCreator versionCreator,
IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator)
{
_routeKeyCreator = routeKeyCreator;
_loadBalancerOptionsCreator = loadBalancerOptionsCreator;
Expand All @@ -56,6 +57,7 @@ IVersionCreator versionCreator
_loadBalancerOptionsCreator = loadBalancerOptionsCreator;
_securityOptionsCreator = securityOptionsCreator;
_versionCreator = versionCreator;
_upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator;
}

public List<Route> Create(FileConfiguration fileConfiguration)
Expand Down Expand Up @@ -150,13 +152,15 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf

private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoutes)
{
var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute);
var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute);
var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute);

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

return route;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Ocelot.Configuration.File;
using Ocelot.Values;

namespace Ocelot.Configuration.Creator;

/// <summary>
/// Default creator of upstream templates based on route headers.
/// </summary>
/// <remarks>Ocelot feature: Routing based on request header.</remarks>
public partial class UpstreamHeaderTemplatePatternCreator : IUpstreamHeaderTemplatePatternCreator
{
raman-m marked this conversation as resolved.
Show resolved Hide resolved
#if NET7_0_OR_GREATER
[GeneratedRegex(@"(\{header:.*?\})", RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")]
raman-m marked this conversation as resolved.
Show resolved Hide resolved
private static partial Regex RegExPlaceholders();
#else
private static readonly Regex RegExPlaceholdersVar = new(@"(\{header:.*?\})", RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000));
raman-m marked this conversation as resolved.
Show resolved Hide resolved
private static Regex RegExPlaceholders() => RegExPlaceholdersVar;
#endif

public Dictionary<string, UpstreamHeaderTemplate> Create(IRoute route)
raman-m marked this conversation as resolved.
Show resolved Hide resolved
{
var result = new Dictionary<string, UpstreamHeaderTemplate>();

foreach (var headerTemplate in route.UpstreamHeaderTemplates)
{
var headerTemplateValue = headerTemplate.Value;
var matches = RegExPlaceholders().Matches(headerTemplateValue);

if (matches.Count > 0)
raman-m marked this conversation as resolved.
Show resolved Hide resolved
{
var placeholders = matches.Select(m => m.Groups[1].Value).ToArray();
for (int i = 0; i < placeholders.Length; i++)
{
var indexOfPlaceholder = headerTemplateValue.IndexOf(placeholders[i]);
var placeholderName = placeholders[i][8..^1]; // remove "{header:" and "}"
headerTemplateValue = headerTemplateValue.Replace(placeholders[i], $"(?<{placeholderName}>.+)");
}
}

var template = route.RouteIsCaseSensitive
? $"^{headerTemplateValue}$"
: $"^(?i){headerTemplateValue}$"; // ignore case

result.Add(headerTemplate.Key, new(template, headerTemplate.Value));
}

return result;
}
}
9 changes: 6 additions & 3 deletions src/Ocelot/Configuration/DownstreamRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public DownstreamRoute(
bool dangerousAcceptAnyServerCertificateValidator,
SecurityOptions securityOptions,
string downstreamHttpMethod,
Version downstreamHttpVersion)
Version downstreamHttpVersion,
Dictionary<string, UpstreamHeaderTemplate> upstreamHeaders)
{
DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator;
AddHeadersToDownstream = addHeadersToDownstream;
Expand Down Expand Up @@ -74,7 +75,8 @@ public DownstreamRoute(
AddHeadersToUpstream = addHeadersToUpstream;
SecurityOptions = securityOptions;
DownstreamHttpMethod = downstreamHttpMethod;
DownstreamHttpVersion = downstreamHttpVersion;
DownstreamHttpVersion = downstreamHttpVersion;
UpstreamHeaders = upstreamHeaders ?? new();
}

public string Key { get; }
Expand Down Expand Up @@ -110,6 +112,7 @@ public DownstreamRoute(
public bool DangerousAcceptAnyServerCertificateValidator { get; }
public SecurityOptions SecurityOptions { get; }
public string DownstreamHttpMethod { get; }
public Version DownstreamHttpVersion { get; }
public Version DownstreamHttpVersion { get; }
public Dictionary<string, UpstreamHeaderTemplate> UpstreamHeaders { get; }
}
}
15 changes: 12 additions & 3 deletions src/Ocelot/Configuration/File/FileAggregateRoute.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Http;

namespace Ocelot.Configuration.File
{
public class FileAggregateRoute : IRoute
Expand All @@ -10,8 +12,15 @@ public class FileAggregateRoute : IRoute
public string Aggregator { get; set; }

// Only supports GET..are you crazy!! POST, PUT WOULD BE CRAZY!! :)
public List<string> UpstreamHttpMethod => new() { "Get" };

public int Priority { get; set; } = 1;
public List<string> UpstreamHttpMethod => new() { HttpMethods.Get };
public Dictionary<string, string> UpstreamHeaderTemplates { get; set; }
public int Priority { get; set; } = 1;

public FileAggregateRoute()
{
RouteKeys = new();
RouteKeysConfig = new();
UpstreamHeaderTemplates = new();
}
}
}
5 changes: 4 additions & 1 deletion src/Ocelot/Configuration/File/FileRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public FileRoute()
QoSOptions = new FileQoSOptions();
RateLimitOptions = new FileRateLimitRule();
RouteClaimsRequirement = new Dictionary<string, string>();
SecurityOptions = new FileSecurityOptions();
SecurityOptions = new FileSecurityOptions();
UpstreamHeaderTemplates = new Dictionary<string, string>();
UpstreamHeaderTransform = new Dictionary<string, string>();
UpstreamHttpMethod = new List<string>();
}
Expand Down Expand Up @@ -60,6 +61,7 @@ public FileRoute(FileRoute from)
public string UpstreamHost { get; set; }
public List<string> UpstreamHttpMethod { get; set; }
public string UpstreamPathTemplate { get; set; }
public Dictionary<string, string> UpstreamHeaderTemplates { get; set; }
raman-m marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Clones this object by making a deep copy.
Expand Down Expand Up @@ -101,6 +103,7 @@ public static void DeepCopy(FileRoute from, FileRoute to)
to.ServiceName = from.ServiceName;
to.ServiceNamespace = from.ServiceNamespace;
to.Timeout = from.Timeout;
to.UpstreamHeaderTemplates = new(from.UpstreamHeaderTemplates);
to.UpstreamHeaderTransform = new(from.UpstreamHeaderTransform);
to.UpstreamHost = from.UpstreamHost;
to.UpstreamHttpMethod = new(from.UpstreamHttpMethod);
Expand Down
Loading