Skip to content

Commit 6cef42e

Browse files
jlukawskaraman-m
andcommitted
#360 Routing based on request header (#1312)
* routing based on headers (all specified headers must match) * routing based on headers for aggregated routes * unit tests and small modifications * find placeholders in header templates * match upstream headers to header templates * find placeholders name and values, fix regex for finding placeholders values * fix unit tests * change header placeholder pattern * unit tests * unit tests * unit tests * unit tests * extend validation with checking upstreamheadertemplates, acceptance tests for cases from the issue * update docs and minor changes * SA1649 File name should match first type name * Fix compilation errors by code review after resolving conflicts * Fix warnings * File-scoped namespaces * File-scoped namespace * Target-typed 'new' expressions (C# 9). https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/target-typed-new * IDE1006 Naming rule violation: These words must begin with upper case characters: should_* * Target-typed 'new' expressions (C# 9). https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/target-typed-new * Fix build errors * DownstreamRouteBuilder * AggregatesCreator * IUpstreamHeaderTemplatePatternCreator, RoutesCreator * UpstreamHeaderTemplatePatternCreator * FileAggregateRoute * FileAggregateRoute * FileRoute * Route, IRoute * FileConfigurationFluentValidator * OcelotBuilder * DownstreamRouteCreator * DownstreamRouteFinder * HeaderMatcher * DownstreamRouteFinderMiddleware * UpstreamHeaderTemplate * Routing folder * RoutingBasedOnHeadersTests * Refactor acceptance tests * AAA pattern in unit tests * CS8936: Feature 'collection expressions' is not available in C# 10.0. Please use language version 12.0 or greater. * Code review by @RaynaldM * Convert facts to one `Theory` * AAA pattern * Add traits * Update routing.rst Check grammar and style * Update docs --------- Co-authored-by: raman-m <[email protected]>
1 parent 6e9a975 commit 6cef42e

39 files changed

+2092
-489
lines changed

docs/features/configuration.rst

+6-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ Here is an example Route configuration. You don't need to set all of these thing
1919
.. code-block:: json
2020
2121
{
22-
"DownstreamPathTemplate": "/",
2322
"UpstreamPathTemplate": "/",
23+
"UpstreamHeaderTemplates": {}, // dictionary
24+
"UpstreamHost": "",
2425
"UpstreamHttpMethod": [ "Get" ],
26+
"DownstreamPathTemplate": "/",
2527
"DownstreamHttpMethod": "",
2628
"DownstreamHttpVersion": "",
2729
"AddHeadersToRequest": {},
@@ -37,7 +39,7 @@ Here is an example Route configuration. You don't need to set all of these thing
3739
"ServiceName": "",
3840
"DownstreamScheme": "http",
3941
"DownstreamHostAndPorts": [
40-
{ "Host": "localhost", "Port": 51876 }
42+
{ "Host": "localhost", "Port": 12345 }
4143
],
4244
"QoSOptions": {
4345
"ExceptionsAllowedBeforeBreaking": 0,
@@ -70,7 +72,8 @@ Here is an example Route configuration. You don't need to set all of these thing
7072
}
7173
}
7274
73-
More information on how to use these options is below.
75+
The actual Route schema for properties can be found in the C# `FileRoute <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileRoute.cs>`_ class.
76+
If you're interested in learning more about how to utilize these options, read below!
7477

7578
Multiple Environments
7679
---------------------

docs/features/routing.rst

+57-4
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,58 @@ The Route above will only be matched when the ``Host`` header value is ``somedom
154154
If you do not set **UpstreamHost** on a Route then any ``Host`` header will match it.
155155
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.
156156

157+
.. _routing-upstream-headers:
158+
159+
Upstream Headers [#f3]_
160+
-----------------------
161+
162+
In addition to routing by ``UpstreamPathTemplate``, you can also define ``UpstreamHeaderTemplates``.
163+
For a route to match, all headers specified in this dictionary object must be present in the request headers.
164+
165+
.. code-block:: json
166+
167+
{
168+
// ...
169+
"UpstreamPathTemplate": "/",
170+
"UpstreamHttpMethod": [ "Get" ],
171+
"UpstreamHeaderTemplates": { // dictionary
172+
"country": "uk", // 1st header
173+
"version": "v1" // 2nd header
174+
}
175+
}
176+
177+
In this scenario, the route will only match if a request includes both headers with the specified values.
178+
179+
Header placeholders
180+
^^^^^^^^^^^^^^^^^^^
181+
182+
Let's explore a more intriguing scenario where placeholders can be effectively utilized within your ``UpstreamHeaderTemplates``.
183+
184+
Consider the following approach using the special placeholder format ``{header:placeholdername}``:
185+
186+
.. code-block:: json
187+
188+
{
189+
"DownstreamPathTemplate": "/{versionnumber}/api", // with placeholder
190+
"DownstreamScheme": "https",
191+
"DownstreamHostAndPorts": [
192+
{ "Host": "10.0.10.1", "Port": 80 }
193+
],
194+
"UpstreamPathTemplate": "/api",
195+
"UpstreamHttpMethod": [ "Get" ],
196+
"UpstreamHeaderTemplates": {
197+
"version": "{header:versionnumber}" // 'header:' prefix vs placeholder
198+
}
199+
}
200+
201+
In this scenario, the entire value of the request header "**version**" is inserted into the ``DownstreamPathTemplate``.
202+
If necessary, a more intricate upstream header template can be specified, using placeholders such as ``version-{header:version}_country-{header:country}``.
203+
204+
**Note 1**: Placeholders are not required in ``DownstreamPathTemplate``.
205+
This scenario can be utilized to mandate a specific header regardless of its value.
206+
207+
**Note 2**: Additionally, the ``UpstreamHeaderTemplates`` dictionary options are applicable for :doc:`../features/requestaggregation` as well.
208+
157209
Priority
158210
--------
159211

@@ -294,7 +346,7 @@ Here are two user scenarios.
294346

295347
.. _routing-security-options:
296348

297-
Security Options [#f3]_
349+
Security Options [#f4]_
298350
-----------------------
299351

300352
Ocelot allows you to manage multiple patterns for allowed/blocked IPs using the `IPAddressRange <https://github.com/jsakamoto/ipaddressrange>`_ package
@@ -326,7 +378,7 @@ The current patterns managed are the following:
326378
327379
.. _routing-dynamic:
328380

329-
Dynamic Routing [#f4]_
381+
Dynamic Routing [#f5]_
330382
----------------------
331383

332384
The idea is to enable dynamic routing when using a :doc:`../features/servicediscovery` provider so you don't have to provide the Route config.
@@ -336,5 +388,6 @@ See the :ref:`sd-dynamic-routing` docs if this sounds interesting to you.
336388

337389
.. [#f1] ":ref:`routing-empty-placeholders`" feature is available starting in version `23.0 <https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0>`_, see issue `748 <https://github.com/ThreeMammals/Ocelot/issues/748>`_ and the `23.0 <https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0>`__ release notes for details.
338390
.. [#f2] ":ref:`routing-upstream-host`" feature was requested as part of `issue 216 <https://github.com/ThreeMammals/Ocelot/pull/216>`_.
339-
.. [#f3] ":ref:`routing-security-options`" feature was requested as part of `issue 628 <https://github.com/ThreeMammals/Ocelot/issues/628>`_ (of `12.0.1 <https://github.com/ThreeMammals/Ocelot/releases/tag/12.0.1>`_ version), then redesigned and improved by `issue 1400 <https://github.com/ThreeMammals/Ocelot/issues/1400>`_, and published in version `20.0 <https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0>`_ docs.
340-
.. [#f4] ":ref:`routing-dynamic`" feature was requested as part of `issue 340 <https://github.com/ThreeMammals/Ocelot/issues/340>`_. Complete reference: :ref:`sd-dynamic-routing`.
391+
.. [#f3] ":ref:`routing-upstream-headers`" feature was proposed in `issue 360 <https://github.com/ThreeMammals/Ocelot/issues/360>`_, and released in version `24.0 <https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0>`_.
392+
.. [#f4] ":ref:`routing-security-options`" feature was requested as part of `issue 628 <https://github.com/ThreeMammals/Ocelot/issues/628>`_ (of `12.0.1 <https://github.com/ThreeMammals/Ocelot/releases/tag/12.0.1>`_ version), then redesigned and improved by `issue 1400 <https://github.com/ThreeMammals/Ocelot/issues/1400>`_, and published in version `20.0 <https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0>`_ docs.
393+
.. [#f5] ":ref:`routing-dynamic`" feature was requested as part of `issue 340 <https://github.com/ThreeMammals/Ocelot/issues/340>`_. Complete reference: :ref:`sd-dynamic-routing`.

src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs

+16-6
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ public class DownstreamRouteBuilder
4040
private SecurityOptions _securityOptions;
4141
private string _downstreamHttpMethod;
4242
private Version _downstreamHttpVersion;
43+
private Dictionary<string, UpstreamHeaderTemplate> _upstreamHeaders;
4344

4445
public DownstreamRouteBuilder()
4546
{
46-
_downstreamAddresses = new List<DownstreamHostAndPort>();
47-
_delegatingHandlers = new List<string>();
48-
_addHeadersToDownstream = new List<AddHeader>();
49-
_addHeadersToUpstream = new List<AddHeader>();
47+
_downstreamAddresses = new();
48+
_delegatingHandlers = new();
49+
_addHeadersToDownstream = new();
50+
_addHeadersToUpstream = new();
5051
}
5152

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

8889
public DownstreamRouteBuilder WithUpstreamHttpMethod(List<string> input)
8990
{
90-
_upstreamHttpMethod = input.Count == 0 ? new List<HttpMethod>() : input.Select(x => new HttpMethod(x.Trim())).ToList();
91+
_upstreamHttpMethod = input.Count > 0
92+
? input.Select(x => new HttpMethod(x.Trim())).ToList()
93+
: new();
9194
return this;
9295
}
9396

@@ -259,6 +262,12 @@ public DownstreamRouteBuilder WithDownstreamHttpVersion(Version downstreamHttpVe
259262
return this;
260263
}
261264

265+
public DownstreamRouteBuilder WithUpstreamHeaders(Dictionary<string, UpstreamHeaderTemplate> input)
266+
{
267+
_upstreamHeaders = input;
268+
return this;
269+
}
270+
262271
public DownstreamRoute Build()
263272
{
264273
return new DownstreamRoute(
@@ -295,6 +304,7 @@ public DownstreamRoute Build()
295304
_dangerousAcceptAnyServerCertificateValidator,
296305
_securityOptions,
297306
_downstreamHttpMethod,
298-
_downstreamHttpVersion);
307+
_downstreamHttpVersion,
308+
_upstreamHeaders);
299309
}
300310
}

src/Ocelot/Configuration/Builder/RouteBuilder.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ public class RouteBuilder
1010
private string _upstreamHost;
1111
private List<DownstreamRoute> _downstreamRoutes;
1212
private List<AggregateRouteConfig> _downstreamRoutesConfig;
13-
private string _aggregator;
13+
private string _aggregator;
14+
private IDictionary<string, UpstreamHeaderTemplate> _upstreamHeaders;
1415

1516
public RouteBuilder()
1617
{
@@ -58,6 +59,12 @@ public RouteBuilder WithAggregator(string aggregator)
5859
{
5960
_aggregator = aggregator;
6061
return this;
62+
}
63+
64+
public RouteBuilder WithUpstreamHeaders(IDictionary<string, UpstreamHeaderTemplate> upstreamHeaders)
65+
{
66+
_upstreamHeaders = upstreamHeaders;
67+
return this;
6168
}
6269

6370
public Route Build()
@@ -68,7 +75,8 @@ public Route Build()
6875
_upstreamHttpMethod,
6976
_upstreamTemplatePattern,
7077
_upstreamHost,
71-
_aggregator
78+
_aggregator,
79+
_upstreamHeaders
7280
);
7381
}
7482
}

src/Ocelot/Configuration/Creator/AggregatesCreator.cs

+9-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ namespace Ocelot.Configuration.Creator
55
{
66
public class AggregatesCreator : IAggregatesCreator
77
{
8-
private readonly IUpstreamTemplatePatternCreator _creator;
8+
private readonly IUpstreamTemplatePatternCreator _creator;
9+
private readonly IUpstreamHeaderTemplatePatternCreator _headerCreator;
910

10-
public AggregatesCreator(IUpstreamTemplatePatternCreator creator)
11+
public AggregatesCreator(IUpstreamTemplatePatternCreator creator, IUpstreamHeaderTemplatePatternCreator headerCreator)
1112
{
12-
_creator = creator;
13+
_creator = creator;
14+
_headerCreator = headerCreator;
1315
}
1416

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

38-
var upstreamTemplatePattern = _creator.Create(aggregateRoute);
40+
var upstreamTemplatePattern = _creator.Create(aggregateRoute);
41+
var upstreamHeaderTemplates = _headerCreator.Create(aggregateRoute);
3942

4043
var route = new RouteBuilder()
4144
.WithUpstreamHttpMethod(aggregateRoute.UpstreamHttpMethod)
4245
.WithUpstreamPathTemplate(upstreamTemplatePattern)
4346
.WithDownstreamRoutes(applicableRoutes)
4447
.WithAggregateRouteConfig(aggregateRoute.RouteKeysConfig)
4548
.WithUpstreamHost(aggregateRoute.UpstreamHost)
46-
.WithAggregator(aggregateRoute.Aggregator)
49+
.WithAggregator(aggregateRoute.Aggregator)
50+
.WithUpstreamHeaders(upstreamHeaderTemplates)
4751
.Build();
4852

4953
return route;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Ocelot.Configuration.File;
2+
using Ocelot.Values;
3+
4+
namespace Ocelot.Configuration.Creator;
5+
6+
/// <summary>
7+
/// Ocelot feature: <see href="https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/routing.rst#upstream-headers">Routing based on request header</see>.
8+
/// </summary>
9+
public interface IUpstreamHeaderTemplatePatternCreator
10+
{
11+
/// <summary>
12+
/// Creates upstream templates based on route headers.
13+
/// </summary>
14+
/// <param name="route">The route info.</param>
15+
/// <returns>An <see cref="IDictionary{TKey, TValue}"/> object where TKey is <see langword="string"/>, TValue is <see cref="UpstreamHeaderTemplate"/>.</returns>
16+
IDictionary<string, UpstreamHeaderTemplate> Create(IRoute route);
17+
}

src/Ocelot/Configuration/Creator/RoutesCreator.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Ocelot.Configuration.Builder;
1+
using Ocelot.Configuration.Builder;
22
using Ocelot.Configuration.File;
33

44
namespace Ocelot.Configuration.Creator
@@ -9,6 +9,7 @@ public class RoutesCreator : IRoutesCreator
99
private readonly IClaimsToThingCreator _claimsToThingCreator;
1010
private readonly IAuthenticationOptionsCreator _authOptionsCreator;
1111
private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator;
12+
private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator;
1213
private readonly IRequestIdKeyCreator _requestIdKeyCreator;
1314
private readonly IQoSOptionsCreator _qosOptionsCreator;
1415
private readonly IRouteOptionsCreator _fileRouteOptionsCreator;
@@ -36,7 +37,8 @@ public RoutesCreator(
3637
ILoadBalancerOptionsCreator loadBalancerOptionsCreator,
3738
IRouteKeyCreator routeKeyCreator,
3839
ISecurityOptionsCreator securityOptionsCreator,
39-
IVersionCreator versionCreator)
40+
IVersionCreator versionCreator,
41+
IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator)
4042
{
4143
_routeKeyCreator = routeKeyCreator;
4244
_loadBalancerOptionsCreator = loadBalancerOptionsCreator;
@@ -54,6 +56,7 @@ public RoutesCreator(
5456
_loadBalancerOptionsCreator = loadBalancerOptionsCreator;
5557
_securityOptionsCreator = securityOptionsCreator;
5658
_versionCreator = versionCreator;
59+
_upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator;
5760
}
5861

5962
public List<Route> Create(FileConfiguration fileConfiguration)
@@ -149,12 +152,14 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf
149152
private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoutes)
150153
{
151154
var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute);
155+
var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute);
152156

153157
var route = new RouteBuilder()
154158
.WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod)
155159
.WithUpstreamPathTemplate(upstreamTemplatePattern)
156160
.WithDownstreamRoute(downstreamRoutes)
157161
.WithUpstreamHost(fileRoute.UpstreamHost)
162+
.WithUpstreamHeaders(upstreamHeaderTemplates)
158163
.Build();
159164

160165
return route;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using Ocelot.Configuration.File;
2+
using Ocelot.Values;
3+
4+
namespace Ocelot.Configuration.Creator;
5+
6+
/// <summary>
7+
/// Default creator of upstream templates based on route headers.
8+
/// </summary>
9+
/// <remarks>Ocelot feature: Routing based on request header.</remarks>
10+
public partial class UpstreamHeaderTemplatePatternCreator : IUpstreamHeaderTemplatePatternCreator
11+
{
12+
private const string PlaceHolderPattern = @"(\{header:.*?\})";
13+
#if NET7_0_OR_GREATER
14+
[GeneratedRegex(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")]
15+
private static partial Regex RegExPlaceholders();
16+
#else
17+
private static readonly Regex RegExPlaceholdersVar = new(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000));
18+
private static Regex RegExPlaceholders() => RegExPlaceholdersVar;
19+
#endif
20+
21+
public IDictionary<string, UpstreamHeaderTemplate> Create(IRoute route)
22+
{
23+
var result = new Dictionary<string, UpstreamHeaderTemplate>();
24+
25+
foreach (var headerTemplate in route.UpstreamHeaderTemplates)
26+
{
27+
var headerTemplateValue = headerTemplate.Value;
28+
var matches = RegExPlaceholders().Matches(headerTemplateValue);
29+
30+
if (matches.Count > 0)
31+
{
32+
var placeholders = matches.Select(m => m.Groups[1].Value).ToArray();
33+
for (int i = 0; i < placeholders.Length; i++)
34+
{
35+
var indexOfPlaceholder = headerTemplateValue.IndexOf(placeholders[i]);
36+
var placeholderName = placeholders[i][8..^1]; // remove "{header:" and "}"
37+
headerTemplateValue = headerTemplateValue.Replace(placeholders[i], $"(?<{placeholderName}>.+)");
38+
}
39+
}
40+
41+
var template = route.RouteIsCaseSensitive
42+
? $"^{headerTemplateValue}$"
43+
: $"^(?i){headerTemplateValue}$"; // ignore case
44+
45+
result.Add(headerTemplate.Key, new(template, headerTemplate.Value));
46+
}
47+
48+
return result;
49+
}
50+
}

src/Ocelot/Configuration/DownstreamRoute.cs

+6-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ public DownstreamRoute(
3939
bool dangerousAcceptAnyServerCertificateValidator,
4040
SecurityOptions securityOptions,
4141
string downstreamHttpMethod,
42-
Version downstreamHttpVersion)
42+
Version downstreamHttpVersion,
43+
Dictionary<string, UpstreamHeaderTemplate> upstreamHeaders)
4344
{
4445
DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator;
4546
AddHeadersToDownstream = addHeadersToDownstream;
@@ -74,7 +75,8 @@ public DownstreamRoute(
7475
AddHeadersToUpstream = addHeadersToUpstream;
7576
SecurityOptions = securityOptions;
7677
DownstreamHttpMethod = downstreamHttpMethod;
77-
DownstreamHttpVersion = downstreamHttpVersion;
78+
DownstreamHttpVersion = downstreamHttpVersion;
79+
UpstreamHeaders = upstreamHeaders ?? new();
7880
}
7981

8082
public string Key { get; }
@@ -110,6 +112,7 @@ public DownstreamRoute(
110112
public bool DangerousAcceptAnyServerCertificateValidator { get; }
111113
public SecurityOptions SecurityOptions { get; }
112114
public string DownstreamHttpMethod { get; }
113-
public Version DownstreamHttpVersion { get; }
115+
public Version DownstreamHttpVersion { get; }
116+
public Dictionary<string, UpstreamHeaderTemplate> UpstreamHeaders { get; }
114117
}
115118
}

0 commit comments

Comments
 (0)