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

Manage caching options: EnableHeadersHashing vs CleanableHashingRegexes #2233

Open
wants to merge 3 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
30 changes: 26 additions & 4 deletions src/Ocelot/Cache/DefaultCacheKeyGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,41 @@ public async ValueTask<string> GenerateRequestCacheKey(DownstreamRequest downstr
}
}

if (!options.EnableContentHashing || !downstreamRequest.HasContent)
if (!options.EnableHeadersHashing && !options.EnableContentHashing && !downstreamRequest.HasContent)
{
return MD5Helper.GenerateMd5(builder.ToString());
}

var requestContentString = await ReadContentAsync(downstreamRequest);
builder.Append(Delimiter)
.Append(requestContentString);
if (options.EnableContentHashing)
{
var requestContentString = await ReadContentAsync(downstreamRequest);
builder.Append(Delimiter)
.Append(requestContentString);
}

if (options.EnableHeadersHashing)
{
var requestHeadersString = await ReadHeadersAsync(downstreamRequest);
builder.Append(Delimiter)
.Append(requestHeadersString);
}

if (options.CleanableHashingRegexes.Any())
{
return MD5Helper.GenerateMd5(HashingClean(builder.ToString(), options.CleanableHashingRegexes));
}

return MD5Helper.GenerateMd5(builder.ToString());
}

private static string HashingClean(string input, List<string> patterns) =>
patterns.Aggregate(input, (current, pattern) => Regex.Replace(current, pattern, string.Empty, RegexOptions.Singleline));

private static Task<string> ReadContentAsync(DownstreamRequest downstream) => downstream.HasContent
? downstream?.Request?.Content?.ReadAsStringAsync() ?? Task.FromResult(string.Empty)
: Task.FromResult(string.Empty);

private static Task<string> ReadHeadersAsync(DownstreamRequest downstream) => downstream.HasContent
? Task.FromResult(string.Join(":", downstream?.Headers.Select(h => h.Key + "=" + string.Join(",", h.Value))))
: Task.FromResult(string.Empty);
}
18 changes: 16 additions & 2 deletions src/Ocelot/Configuration/CacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ internal CacheOptions() { }
/// <param name="ttlSeconds">Time-to-live seconds. If not speciefied, zero value is used by default.</param>
/// <param name="region">The region of caching.</param>
/// <param name="header">The header name to control cached value.</param>
/// <param name="enableContentHashing">The switcher for content hashing. If not speciefied, false value is used by default.</param>
public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing)
/// <param name="enableContentHashing">The switcher for content hashing. If not speciefied, false value is used by default.</param>
/// <param name="enableHeadersHashing">The switcher for headers hashing. If not speciefied, false value is used by default.</param>
/// <param name="cleanableHashingRegexes">The list of regex patterns to clean the hash. If not speciefied, an empty list is used by default.</param></param>
public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing, bool? enableHeadersHashing, List<string>? cleanableHashingRegexes)
{
TtlSeconds = ttlSeconds ?? 0;
Region = region;
Header = header;
EnableContentHashing = enableContentHashing ?? false;
EnableHeadersHashing = enableHeadersHashing ?? false;
CleanableHashingRegexes = cleanableHashingRegexes ?? new();
}

/// <summary>Time-to-live seconds.</summary>
Expand All @@ -39,4 +43,14 @@ public CacheOptions(int? ttlSeconds, string region, string header, bool? enableC
/// <remarks>Default value is <see langword="false"/>. No hashing by default.</remarks>
/// <value><see langword="true"/> if hashing is enabled, otherwise it is <see langword="false"/>.</value>
public bool EnableContentHashing { get; }

/// <summary>Enables MD5 hash calculation of the <see cref="HttpRequestMessage.Headers"/> of the <see cref="DownstreamRequest.Request"/> object.</summary>
/// <remarks>Default value is <see langword="false"/>. No hashing by default.</remarks>
/// <value><see langword="true"/> if hashing is enabled, otherwise it is <see langword="false"/>.</value>
public bool EnableHeadersHashing { get; }

/// <summary>The list of regex patterns to clean the hash.</summary>
/// <remarks>Default value is an empty list. No cleaning by default.</remarks>
/// <value>A <see cref="List{T}"/> of <see cref="string"/> values.</value>
public List<string> CleanableHashingRegexes { get; }
}
4 changes: 3 additions & 1 deletion src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ public CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration glo
var header = options.Header ?? global?.CacheOptions.Header;
var ttlSeconds = options.TtlSeconds ?? global?.CacheOptions.TtlSeconds;
var enableContentHashing = options.EnableContentHashing ?? global?.CacheOptions.EnableContentHashing;
var enableHeadersHashing = options.EnableHeadersHashing ?? global?.CacheOptions.EnableHeadersHashing;
var cleanableHashingRegexes = options.CleanableHashingRegexes ?? global?.CacheOptions.CleanableHashingRegexes;

return new CacheOptions(ttlSeconds, region, header, enableContentHashing);
return new CacheOptions(ttlSeconds, region, header, enableContentHashing, enableHeadersHashing, cleanableHashingRegexes);
}

protected virtual string GetRegion(string region, string upstreamPathTemplate, IList<string> upstreamHttpMethod)
Expand Down
12 changes: 12 additions & 0 deletions src/Ocelot/Configuration/File/FileCacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public FileCacheOptions(FileCacheOptions from)
TtlSeconds = from.TtlSeconds;
Header = from.Header;
EnableContentHashing = from.EnableContentHashing;
EnableHeadersHashing = from.EnableHeadersHashing;
CleanableHashingRegexes = from.CleanableHashingRegexes;
}

/// <summary>Using <see cref="Nullable{T}"/> where T is <see cref="int"/> to have <see langword="null"/> as default value and allowing global configuration usage.</summary>
Expand All @@ -23,4 +25,14 @@ public FileCacheOptions(FileCacheOptions from)
/// <remarks>If <see langword="null"/> then use global configuration with <see langword="false"/> by default.</remarks>
/// <value><see langword="true"/> if content hashing is enabled; otherwise, <see langword="false"/>.</value>
public bool? EnableContentHashing { get; set; }

/// <summary>Using <see cref="Nullable{T}"/> where T is <see cref="bool"/> to have <see langword="null"/> as default value and allowing global configuration usage.</summary>
/// <remarks>If <see langword="null"/> then use global configuration with <see langword="false"/> by default.</remarks>
/// <value><see langword="true"/> if headers hashing is enabled; otherwise, <see langword="false"/>.</value>
public bool? EnableHeadersHashing { get; set; }

/// <summary>Using <see cref="Nullable{T}"/> where T is <see cref="List{T}"/> to have <see langword="null"/> as default value and allowing global configuration usage.</summary>
/// <remarks>If <see langword="null"/> then use global configuration with empty list by default.</remarks>
/// <value>The list of regular expressions for cleanable hashing.</value>
public List<string>? CleanableHashingRegexes { get; set; }
}
6 changes: 3 additions & 3 deletions test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void should_generate_cache_key_with_request_content()
const string noHeader = null;
const string content = nameof(should_generate_cache_key_with_request_content);
var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{content}");
CacheOptions options = new CacheOptions(100, "region", noHeader, true);
CacheOptions options = new CacheOptions(100, "region", noHeader, true, false, null);

this.Given(x => x.GivenDownstreamRoute(options))
.And(x => GivenHasContent(content))
Expand All @@ -59,7 +59,7 @@ public void should_generate_cache_key_without_request_content()
[Fact]
public void should_generate_cache_key_with_cache_options_header()
{
CacheOptions options = new CacheOptions(100, "region", headerName, false);
CacheOptions options = new CacheOptions(100, "region", headerName, false, false, null);
var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{header}");

this.Given(x => x.GivenDownstreamRoute(options))
Expand All @@ -72,7 +72,7 @@ public void should_generate_cache_key_with_cache_options_header()
public void should_generate_cache_key_happy_path()
{
const string content = nameof(should_generate_cache_key_happy_path);
CacheOptions options = new CacheOptions(100, "region", headerName, true);
CacheOptions options = new CacheOptions(100, "region", headerName, true, false, null);
var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{header}-{content}");

this.Given(x => x.GivenDownstreamRoute(options))
Expand Down
2 changes: 1 addition & 1 deletion test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ private void GivenTheDownstreamRouteIs()
var route = new RouteBuilder()
.WithDownstreamRoute(new DownstreamRouteBuilder()
.WithIsCached(true)
.WithCacheOptions(new CacheOptions(100, "kanken", null, false))
.WithCacheOptions(new CacheOptions(100, "kanken", null, false, false, null))
.WithUpstreamHttpMethod(new List<string> { "Get" })
.Build())
.WithUpstreamHttpMethod(new List<string> { "Get" })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private void GivenTheDownstreamRouteIs()
{
var route = new DownstreamRouteBuilder()
.WithIsCached(true)
.WithCacheOptions(new CacheOptions(100, "kanken", null, false))
.WithCacheOptions(new CacheOptions(100, "kanken", null, false, false, null))
.WithUpstreamHttpMethod(new List<string> { "Get" })
.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public DownstreamRouteExtensionsTests()
null,
null,
default,
new CacheOptions(0, null, null, null),
new CacheOptions(0, null, null, null, null, null),
new LoadBalancerOptions(null, null, 0),
new RateLimitOptions(false, null, null, false, null, null, null, 0),
new Dictionary<string, string>(),
Expand Down
2 changes: 1 addition & 1 deletion test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ private void GivenTheDependenciesAreSetUpCorrectly()
_qoso = new QoSOptionsBuilder().Build();
_rlo = new RateLimitOptionsBuilder().Build();

_cacheOptions = new CacheOptions(0, "vesty", null, false);
_cacheOptions = new CacheOptions(0, "vesty", null, false, false, null);
_hho = new HttpHandlerOptionsBuilder().Build();
_ht = new HeaderTransformations(new List<HeaderFindAndReplace>(), new List<HeaderFindAndReplace>(), new List<AddHeader>(), new List<AddHeader>());
_dhp = new List<DownstreamHostAndPort>();
Expand Down