diff --git a/src/Ocelot/Authorization/ClaimsAuthorizer.cs b/src/Ocelot/Authorization/ClaimsAuthorizer.cs
index 7f1f150a8..ce27e331b 100644
--- a/src/Ocelot/Authorization/ClaimsAuthorizer.cs
+++ b/src/Ocelot/Authorization/ClaimsAuthorizer.cs
@@ -1,87 +1,97 @@
using Ocelot.DownstreamRouteFinder.UrlMatcher;
+using Ocelot.Infrastructure;
using Ocelot.Infrastructure.Claims.Parser;
using Ocelot.Responses;
using System.Security.Claims;
-namespace Ocelot.Authorization
+namespace Ocelot.Authorization;
+
+///
+/// Default authorizer by claims.
+///
+public partial class ClaimsAuthorizer : IClaimsAuthorizer
{
- public class ClaimsAuthorizer : IClaimsAuthorizer
+ private readonly IClaimsParser _claimsParser;
+
+ public ClaimsAuthorizer(IClaimsParser claimsParser)
{
- private readonly IClaimsParser _claimsParser;
+ _claimsParser = claimsParser;
+ }
- public ClaimsAuthorizer(IClaimsParser claimsParser)
+#if NET7_0_OR_GREATER
+ [GeneratedRegex(@"^{(?.+)}$", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
+ private static partial Regex RegexAuthorize();
+#else
+ private static readonly Regex _regexAuthorize = RegexGlobal.New(@"^{(?.+)}$");
+ private static Regex RegexAuthorize() => _regexAuthorize;
+#endif
+ public Response Authorize(
+ ClaimsPrincipal claimsPrincipal,
+ Dictionary routeClaimsRequirement,
+ List urlPathPlaceholderNameAndValues
+ )
+ {
+ foreach (var required in routeClaimsRequirement)
{
- _claimsParser = claimsParser;
- }
+ var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, required.Key);
- public Response Authorize(
- ClaimsPrincipal claimsPrincipal,
- Dictionary routeClaimsRequirement,
- List urlPathPlaceholderNameAndValues
- )
- {
- foreach (var required in routeClaimsRequirement)
+ if (values.IsError)
{
- var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, required.Key);
+ return new ErrorResponse(values.Errors);
+ }
- if (values.IsError)
+ if (values.Data != null)
+ {
+ // dynamic claim
+ var match = RegexAuthorize().Match(required.Value);
+ if (match.Success)
{
- return new ErrorResponse(values.Errors);
- }
+ var variableName = match.Captures[0].Value;
- if (values.Data != null)
- {
- // dynamic claim
- var match = Regex.Match(required.Value, @"^{(?.+)}$");
- if (match.Success)
+ var matchingPlaceholders = urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Take(2).ToArray();
+ if (matchingPlaceholders.Length == 1)
{
- var variableName = match.Captures[0].Value;
-
- var matchingPlaceholders = urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Take(2).ToArray();
- if (matchingPlaceholders.Length == 1)
- {
- // match
- var actualValue = matchingPlaceholders[0].Value;
- var authorized = values.Data.Contains(actualValue);
- if (!authorized)
- {
- return new ErrorResponse(new ClaimValueNotAuthorizedError(
- $"dynamic claim value for {variableName} of {string.Join(", ", values.Data)} is not the same as required value: {actualValue}"));
- }
- }
- else
+ // match
+ var actualValue = matchingPlaceholders[0].Value;
+ var authorized = values.Data.Contains(actualValue);
+ if (!authorized)
{
- // config error
- if (matchingPlaceholders.Length == 0)
- {
- return new ErrorResponse(new ClaimValueNotAuthorizedError(
- $"config error: requires variable claim value: {variableName} placeholders does not contain that variable: {string.Join(", ", urlPathPlaceholderNameAndValues.Select(p => p.Name))}"));
- }
- else
- {
- return new ErrorResponse(new ClaimValueNotAuthorizedError(
- $"config error: requires variable claim value: {required.Value} but placeholders are ambiguous: {string.Join(", ", urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Select(p => p.Value))}"));
- }
+ return new ErrorResponse(new ClaimValueNotAuthorizedError(
+ $"dynamic claim value for {variableName} of {string.Join(", ", values.Data)} is not the same as required value: {actualValue}"));
}
}
else
{
- // static claim
- var authorized = values.Data.Contains(required.Value);
- if (!authorized)
+ // config error
+ if (matchingPlaceholders.Length == 0)
+ {
+ return new ErrorResponse(new ClaimValueNotAuthorizedError(
+ $"config error: requires variable claim value: {variableName} placeholders does not contain that variable: {string.Join(", ", urlPathPlaceholderNameAndValues.Select(p => p.Name))}"));
+ }
+ else
{
return new ErrorResponse(new ClaimValueNotAuthorizedError(
- $"claim value: {string.Join(", ", values.Data)} is not the same as required value: {required.Value} for type: {required.Key}"));
+ $"config error: requires variable claim value: {required.Value} but placeholders are ambiguous: {string.Join(", ", urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Select(p => p.Value))}"));
}
}
}
else
{
- return new ErrorResponse(new UserDoesNotHaveClaimError($"user does not have claim {required.Key}"));
+ // static claim
+ var authorized = values.Data.Contains(required.Value);
+ if (!authorized)
+ {
+ return new ErrorResponse(new ClaimValueNotAuthorizedError(
+ $"claim value: {string.Join(", ", values.Data)} is not the same as required value: {required.Value} for type: {required.Key}"));
+ }
}
}
-
- return new OkResponse(true);
+ else
+ {
+ return new ErrorResponse(new UserDoesNotHaveClaimError($"user does not have claim {required.Key}"));
+ }
}
+
+ return new OkResponse(true);
}
}
diff --git a/src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs
index 52c653f5e..a6129f7ee 100644
--- a/src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs
+++ b/src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs
@@ -1,4 +1,5 @@
using Ocelot.Configuration.File;
+using Ocelot.Infrastructure;
using Ocelot.Values;
namespace Ocelot.Configuration.Creator;
@@ -11,11 +12,11 @@ public partial class UpstreamHeaderTemplatePatternCreator : IUpstreamHeaderTempl
{
private const string PlaceHolderPattern = @"(\{header:.*?\})";
#if NET7_0_OR_GREATER
- [GeneratedRegex(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")]
- private static partial Regex RegExPlaceholders();
+ [GeneratedRegex(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, "en-US")]
+ private static partial Regex RegexPlaceholders();
#else
- private static readonly Regex RegExPlaceholdersVar = new(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000));
- private static Regex RegExPlaceholders() => RegExPlaceholdersVar;
+ private static readonly Regex _regexPlaceholders = RegexGlobal.New(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
+ private static Regex RegexPlaceholders() => _regexPlaceholders;
#endif
public IDictionary Create(IRoute route)
@@ -25,7 +26,7 @@ public IDictionary Create(IRoute route)
foreach (var headerTemplate in route.UpstreamHeaderTemplates)
{
var headerTemplateValue = headerTemplate.Value;
- var matches = RegExPlaceholders().Matches(headerTemplateValue);
+ var matches = RegexPlaceholders().Matches(headerTemplateValue);
if (matches.Count > 0)
{
diff --git a/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs b/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs
index c00571058..ea8b9289d 100644
--- a/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs
+++ b/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs
@@ -1,57 +1,69 @@
-using Ocelot.Responses;
+using Ocelot.Infrastructure;
+using Ocelot.Responses;
-namespace Ocelot.Configuration.Parser
+namespace Ocelot.Configuration.Parser;
+
+///
+/// Default implementation of the interface.
+///
+public partial class ClaimToThingConfigurationParser : IClaimToThingConfigurationParser
{
- public class ClaimToThingConfigurationParser : IClaimToThingConfigurationParser
- {
- private readonly Regex _claimRegex = new("Claims\\[.*\\]");
- private readonly Regex _indexRegex = new("value\\[.*\\]");
- private const char SplitToken = '>';
+ private const char SplitToken = '>';
+#if NET7_0_OR_GREATER
+ [GeneratedRegex("Claims\\[.*\\]", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
+ private static partial Regex ClaimRegex();
+ [GeneratedRegex("value\\[.*\\]", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
+ private static partial Regex IndexRegex();
+#else
+ private static readonly Regex _claimRegex = RegexGlobal.New("Claims\\[.*\\]");
+ private static readonly Regex _indexRegex = RegexGlobal.New("value\\[.*\\]");
+ private static Regex ClaimRegex() => _claimRegex;
+ private static Regex IndexRegex() => _indexRegex;
+#endif
- public Response Extract(string existingKey, string value)
+ public Response Extract(string existingKey, string value)
+ {
+ try
{
- try
- {
- var instructions = value.Split(SplitToken);
+ var instructions = value.Split(SplitToken);
- if (instructions.Length <= 1)
- {
- return new ErrorResponse(new NoInstructionsError(SplitToken.ToString()));
- }
-
- var claimMatch = _claimRegex.IsMatch(instructions[0]);
+ if (instructions.Length <= 1)
+ {
+ return new ErrorResponse(new NoInstructionsError(SplitToken.ToString()));
+ }
- if (!claimMatch)
- {
- return new ErrorResponse(new InstructionNotForClaimsError());
- }
+ var claimMatch = ClaimRegex().IsMatch(instructions[0]);
- var newKey = GetIndexValue(instructions[0]);
- var index = 0;
- var delimiter = string.Empty;
+ if (!claimMatch)
+ {
+ return new ErrorResponse(new InstructionNotForClaimsError());
+ }
- if (instructions.Length > 2 && _indexRegex.IsMatch(instructions[1]))
- {
- index = int.Parse(GetIndexValue(instructions[1]));
- delimiter = instructions[2].Trim();
- }
+ var newKey = GetIndexValue(instructions[0]);
+ var index = 0;
+ var delimiter = string.Empty;
- return new OkResponse(
- new ClaimToThing(existingKey, newKey, delimiter, index));
- }
- catch (Exception exception)
+ if (instructions.Length > 2 && IndexRegex().IsMatch(instructions[1]))
{
- return new ErrorResponse(new ParsingConfigurationHeaderError(exception));
+ index = int.Parse(GetIndexValue(instructions[1]));
+ delimiter = instructions[2].Trim();
}
- }
- private static string GetIndexValue(string instruction)
+ return new OkResponse(
+ new ClaimToThing(existingKey, newKey, delimiter, index));
+ }
+ catch (Exception exception)
{
- var firstIndexer = instruction.IndexOf('[', StringComparison.Ordinal);
- var lastIndexer = instruction.IndexOf(']', StringComparison.Ordinal);
- var length = lastIndexer - firstIndexer;
- var claimKey = instruction.Substring(firstIndexer + 1, length - 1);
- return claimKey;
+ return new ErrorResponse(new ParsingConfigurationHeaderError(exception));
}
}
-}
+
+ private static string GetIndexValue(string instruction)
+ {
+ var firstIndexer = instruction.IndexOf('[', StringComparison.Ordinal);
+ var lastIndexer = instruction.IndexOf(']', StringComparison.Ordinal);
+ var length = lastIndexer - firstIndexer;
+ var claimKey = instruction.Substring(firstIndexer + 1, length - 1);
+ return claimKey;
+ }
+}
diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs
index c8596b2d5..36b6e5076 100644
--- a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs
+++ b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs
@@ -2,14 +2,13 @@
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Configuration.File;
using Ocelot.Errors;
+using Ocelot.Infrastructure;
using Ocelot.Responses;
using Ocelot.ServiceDiscovery;
namespace Ocelot.Configuration.Validator
{
- ///
- /// Validation of a objects.
- ///
+ /// Validation of a objects.
public partial class FileConfigurationFluentValidator : AbstractValidator, IConfigurationValidator
{
private const string Servicefabric = "servicefabric";
@@ -101,11 +100,11 @@ private static bool AllRoutesForAggregateExist(FileAggregateRoute fileAggregateR
}
#if NET7_0_OR_GREATER
- [GeneratedRegex(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")]
+ [GeneratedRegex(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, "en-US")]
private static partial Regex PlaceholderRegex();
#else
- private static readonly Regex PlaceholderRegexVar = new(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000));
- private static Regex PlaceholderRegex() => PlaceholderRegexVar;
+ private static readonly Regex _placeholderRegex = RegexGlobal.New(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline);
+ private static Regex PlaceholderRegex() => _placeholderRegex;
#endif
private static bool IsPlaceholderNotDuplicatedIn(string pathTemplate)
diff --git a/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
index fbcbd57d2..ecf798b6b 100644
--- a/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
+++ b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
@@ -2,129 +2,146 @@
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Creator;
+using Ocelot.Infrastructure;
-namespace Ocelot.Configuration.Validator
+namespace Ocelot.Configuration.Validator;
+
+///
+/// Default implementation od the abstract class.
+///
+public partial class RouteFluentValidator : AbstractValidator
{
- public class RouteFluentValidator : AbstractValidator
+ private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
+
+ public RouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, HostAndPortValidator hostAndPortValidator, FileQoSOptionsFluentValidator fileQoSOptionsFluentValidator)
{
- private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
+ _authenticationSchemeProvider = authenticationSchemeProvider;
+
+ RuleFor(route => route.QoSOptions)
+ .SetValidator(fileQoSOptionsFluentValidator);
+
+ RuleFor(route => route.DownstreamPathTemplate)
+ .NotEmpty()
+ .WithMessage("{PropertyName} cannot be empty");
+
+ RuleFor(route => route.UpstreamPathTemplate)
+ .NotEmpty()
+ .WithMessage("{PropertyName} cannot be empty");
- public RouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, HostAndPortValidator hostAndPortValidator, FileQoSOptionsFluentValidator fileQoSOptionsFluentValidator)
+ When(route => !string.IsNullOrEmpty(route.DownstreamPathTemplate), () =>
{
- _authenticationSchemeProvider = authenticationSchemeProvider;
+ RuleFor(route => route.DownstreamPathTemplate)
+ .Must(path => path.StartsWith('/'))
+ .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
- RuleFor(route => route.QoSOptions)
- .SetValidator(fileQoSOptionsFluentValidator);
+ RuleFor(route => route.DownstreamPathTemplate)
+ .Must(path => !path.Contains("//"))
+ .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.");
RuleFor(route => route.DownstreamPathTemplate)
- .NotEmpty()
- .WithMessage("{PropertyName} cannot be empty");
+ .Must(path => !path.Contains("https://") && !path.Contains("http://"))
+ .WithMessage("{PropertyName} {PropertyValue} contains scheme");
+ });
+ When(route => !string.IsNullOrEmpty(route.UpstreamPathTemplate), () =>
+ {
RuleFor(route => route.UpstreamPathTemplate)
- .NotEmpty()
- .WithMessage("{PropertyName} cannot be empty");
-
- When(route => !string.IsNullOrEmpty(route.DownstreamPathTemplate), () =>
- {
- RuleFor(route => route.DownstreamPathTemplate)
- .Must(path => path.StartsWith('/'))
- .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
-
- RuleFor(route => route.DownstreamPathTemplate)
- .Must(path => !path.Contains("//"))
- .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.");
-
- RuleFor(route => route.DownstreamPathTemplate)
- .Must(path => !path.Contains("https://") && !path.Contains("http://"))
- .WithMessage("{PropertyName} {PropertyValue} contains scheme");
- });
-
- When(route => !string.IsNullOrEmpty(route.UpstreamPathTemplate), () =>
- {
- RuleFor(route => route.UpstreamPathTemplate)
- .Must(path => !path.Contains("//"))
- .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.");
-
- RuleFor(route => route.UpstreamPathTemplate)
- .Must(path => path.StartsWith('/'))
- .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
-
- RuleFor(route => route.UpstreamPathTemplate)
- .Must(path => !path.Contains("https://") && !path.Contains("http://"))
- .WithMessage("{PropertyName} {PropertyValue} contains scheme");
- });
-
- When(route => route.RateLimitOptions.EnableRateLimiting, () =>
- {
- RuleFor(route => route.RateLimitOptions.Period)
- .NotEmpty()
- .WithMessage("RateLimitOptions.Period is empty");
-
- RuleFor(route => route.RateLimitOptions)
- .Must(IsValidPeriod)
- .WithMessage("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period");
- });
-
- RuleFor(route => route.AuthenticationOptions)
- .MustAsync(IsSupportedAuthenticationProviders)
- .WithMessage("{PropertyName} {PropertyValue} is unsupported authentication provider");
-
- When(route => string.IsNullOrEmpty(route.ServiceName), () =>
- {
- RuleFor(r => r.DownstreamHostAndPorts).NotEmpty()
- .WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!");
- });
-
- When(route => string.IsNullOrEmpty(route.ServiceName), () =>
- {
- RuleForEach(route => route.DownstreamHostAndPorts)
- .SetValidator(hostAndPortValidator);
- });
-
- When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersion), () =>
- {
- RuleFor(r => r.DownstreamHttpVersion).Matches("^[0-9]([.,][0-9]{1,1})?$");
- });
-
- When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersionPolicy), () =>
- {
- RuleFor(r => r.DownstreamHttpVersionPolicy).Matches($@"^({VersionPolicies.RequestVersionExact}|{VersionPolicies.RequestVersionOrHigher}|{VersionPolicies.RequestVersionOrLower})$");
- });
- }
+ .Must(path => !path.Contains("//"))
+ .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.");
+
+ RuleFor(route => route.UpstreamPathTemplate)
+ .Must(path => path.StartsWith('/'))
+ .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
+
+ RuleFor(route => route.UpstreamPathTemplate)
+ .Must(path => !path.Contains("https://") && !path.Contains("http://"))
+ .WithMessage("{PropertyName} {PropertyValue} contains scheme");
+ });
- private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions options, CancellationToken cancellationToken)
+ When(route => route.RateLimitOptions.EnableRateLimiting, () =>
{
- if (string.IsNullOrEmpty(options.AuthenticationProviderKey)
- && options.AuthenticationProviderKeys.Length == 0)
- {
- return true;
- }
-
- var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync();
- var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList();
- var primary = options.AuthenticationProviderKey;
- return !string.IsNullOrEmpty(primary) && supportedSchemes.Contains(primary)
- || (string.IsNullOrEmpty(primary) && options.AuthenticationProviderKeys.All(supportedSchemes.Contains));
- }
+ RuleFor(route => route.RateLimitOptions.Period)
+ .NotEmpty()
+ .WithMessage("RateLimitOptions.Period is empty");
+
+ RuleFor(route => route.RateLimitOptions)
+ .Must(IsValidPeriod)
+ .WithMessage("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period");
+ });
- private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions)
+ RuleFor(route => route.AuthenticationOptions)
+ .MustAsync(IsSupportedAuthenticationProviders)
+ .WithMessage("{PropertyName} {PropertyValue} is unsupported authentication provider");
+
+ When(route => string.IsNullOrEmpty(route.ServiceName), () =>
{
- if (string.IsNullOrEmpty(rateLimitOptions.Period))
- {
- return false;
- }
+ RuleFor(r => r.DownstreamHostAndPorts).NotEmpty()
+ .WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!");
+ });
- var period = rateLimitOptions.Period.Trim();
+ When(route => string.IsNullOrEmpty(route.ServiceName), () =>
+ {
+ RuleForEach(route => route.DownstreamHostAndPorts)
+ .SetValidator(hostAndPortValidator);
+ });
- var secondsRegEx = new Regex("^[0-9]+s");
- var minutesRegEx = new Regex("^[0-9]+m");
- var hoursRegEx = new Regex("^[0-9]+h");
- var daysRegEx = new Regex("^[0-9]+d");
+ When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersion), () =>
+ {
+ RuleFor(r => r.DownstreamHttpVersion).Matches("^[0-9]([.,][0-9]{1,1})?$");
+ });
- return secondsRegEx.Match(period).Success
- || minutesRegEx.Match(period).Success
- || hoursRegEx.Match(period).Success
- || daysRegEx.Match(period).Success;
+ When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersionPolicy), () =>
+ {
+ RuleFor(r => r.DownstreamHttpVersionPolicy).Matches($@"^({VersionPolicies.RequestVersionExact}|{VersionPolicies.RequestVersionOrHigher}|{VersionPolicies.RequestVersionOrLower})$");
+ });
+ }
+
+ private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions options, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(options.AuthenticationProviderKey)
+ && options.AuthenticationProviderKeys.Length == 0)
+ {
+ return true;
+ }
+
+ var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync();
+ var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList();
+ var primary = options.AuthenticationProviderKey;
+ return !string.IsNullOrEmpty(primary) && supportedSchemes.Contains(primary)
+ || (string.IsNullOrEmpty(primary) && options.AuthenticationProviderKeys.All(supportedSchemes.Contains));
+ }
+
+#if NET7_0_OR_GREATER
+ [GeneratedRegex("^[0-9]+s", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
+ private static partial Regex SecondsRegex();
+ [GeneratedRegex("^[0-9]+m", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
+ private static partial Regex MinutesRegex();
+ [GeneratedRegex("^[0-9]+h", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
+ private static partial Regex HoursRegex();
+ [GeneratedRegex("^[0-9]+d", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
+ private static partial Regex DaysRegex();
+#else
+ private static readonly Regex _secondsRegex = RegexGlobal.New("^[0-9]+s");
+ private static readonly Regex _minutesRegex = RegexGlobal.New("^[0-9]+m");
+ private static readonly Regex _hoursRegex = RegexGlobal.New("^[0-9]+h");
+ private static readonly Regex _daysRegex = RegexGlobal.New("^[0-9]+d");
+ private static Regex SecondsRegex() => _secondsRegex;
+ private static Regex MinutesRegex() => _minutesRegex;
+ private static Regex HoursRegex() => _hoursRegex;
+ private static Regex DaysRegex() => _daysRegex;
+#endif
+
+ private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions)
+ {
+ if (string.IsNullOrEmpty(rateLimitOptions.Period))
+ {
+ return false;
}
+
+ var period = rateLimitOptions.Period.Trim();
+ return SecondsRegex().Match(period).Success
+ || MinutesRegex().Match(period).Success
+ || HoursRegex().Match(period).Success
+ || DaysRegex().Match(period).Success;
}
}
diff --git a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs
index 9101f5b52..13d27f1e7 100644
--- a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs
+++ b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs
@@ -3,6 +3,7 @@
using Microsoft.Extensions.Configuration.Memory;
using Newtonsoft.Json;
using Ocelot.Configuration.File;
+using Ocelot.Infrastructure;
namespace Ocelot.DependencyInjection
{
@@ -15,14 +16,6 @@ public static partial class ConfigurationBuilderExtensions
public const string GlobalConfigFile = "ocelot.global.json";
public const string EnvironmentConfigFile = "ocelot.{0}.json";
-#if NET7_0_OR_GREATER
- [GeneratedRegex(@"^ocelot\.(.*?)\.json$", RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")]
- private static partial Regex SubConfigRegex();
-#else
- private static readonly Regex SubConfigRegexVar = new(@"^ocelot\.(.*?)\.json$", RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000));
- private static Regex SubConfigRegex() => SubConfigRegexVar;
-#endif
-
[Obsolete("Please set BaseUrl in ocelot.json GlobalConfiguration.BaseUrl")]
public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder builder, string baseUrl)
{
@@ -103,9 +96,21 @@ private static IConfigurationBuilder ApplyMergeOcelotJsonOption(IConfigurationBu
AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange);
}
+#if NET7_0_OR_GREATER
+ [GeneratedRegex(@"^ocelot\.(.*?)\.json$", RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, "en-US")]
+ private static partial Regex SubConfigRegex();
+#else
+ private static readonly Regex _subConfigRegex = RegexGlobal.New(@"^ocelot\.(.*?)\.json$", RegexOptions.IgnoreCase | RegexOptions.Singleline);
+ private static Regex SubConfigRegex() => _subConfigRegex;
+#endif
private static string GetMergedOcelotJson(string folder, IWebHostEnvironment env,
FileConfiguration fileConfiguration = null, string primaryFile = null, string globalFile = null, string environmentFile = null)
- {
+ {
+ // All versions of overloaded AddOcelot methods call this GetMergedOcelotJson one, so we improve Regex performance by cache increasing.
+ // Developers can adjust the RegexGlobal value BEFORE calling AddOcelot
+ // Developers can adjust the Regex.CacheSize value AFTER calling AddOcelot
+ Regex.CacheSize = RegexGlobal.RegexCacheSize;
+
var envName = string.IsNullOrEmpty(env?.EnvironmentName) ? "Development" : env.EnvironmentName;
environmentFile ??= Path.Join(folder, string.Format(EnvironmentConfigFile, envName));
var reg = SubConfigRegex();
diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs
index 2a5380c27..cfbcc049e 100644
--- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs
+++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
+using Ocelot.Infrastructure;
using Ocelot.Logging;
using Ocelot.Middleware;
using Ocelot.Request.Middleware;
@@ -118,6 +119,7 @@ private static string MergeQueryStringsWithoutDuplicateValues(string queryString
}
private static string MapQueryParameter(KeyValuePair pair) => $"{pair.Key}={pair.Value}";
+ private static readonly ConcurrentDictionary _regex = new();
private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(DownstreamRequest downstreamRequest, List templatePlaceholderNameAndValues)
{
@@ -125,8 +127,10 @@ private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(Downst
{
var name = nAndV.Name.Trim(OpeningBrace, ClosingBrace);
var value = Regex.Escape(nAndV.Value); // to ensure a placeholder value containing special Regex characters from URL query parameters is safely used in a Regex constructor, it's necessary to escape the value
- var rgx = new Regex($@"\b{name}={value}\b");
-
+ var pattern = $@"\b{name}={value}\b";
+ var rgx = _regex.AddOrUpdate(pattern,
+ RegexGlobal.New(pattern),
+ (key, oldValue) => oldValue);
if (rgx.IsMatch(downstreamRequest.Query))
{
var questionMarkOrAmpersand = downstreamRequest.Query.IndexOf(name, StringComparison.Ordinal);
diff --git a/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs b/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs
index 653222506..510808fae 100644
--- a/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs
+++ b/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs
@@ -2,58 +2,67 @@
using Ocelot.Request.Middleware;
using Ocelot.Responses;
-namespace Ocelot.Infrastructure
+namespace Ocelot.Infrastructure;
+
+///
+/// The configuration related implementation of the interface.
+///
+public partial class ConfigAwarePlaceholders : IPlaceholders
{
- public class ConfigAwarePlaceholders : IPlaceholders
+ private readonly IConfiguration _configuration;
+ private readonly IPlaceholders _placeholders;
+
+ public ConfigAwarePlaceholders(IConfiguration configuration, IPlaceholders placeholders)
{
- private readonly IConfiguration _configuration;
- private readonly IPlaceholders _placeholders;
+ _configuration = configuration;
+ _placeholders = placeholders;
+ }
- public ConfigAwarePlaceholders(IConfiguration configuration, IPlaceholders placeholders)
- {
- _configuration = configuration;
- _placeholders = placeholders;
- }
+ public Response Get(string key)
+ {
+ var placeholderResponse = _placeholders.Get(key);
- public Response Get(string key)
+ if (!placeholderResponse.IsError)
{
- var placeholderResponse = _placeholders.Get(key);
-
- if (!placeholderResponse.IsError)
- {
- return placeholderResponse;
- }
-
- return GetFromConfig(CleanKey(key));
+ return placeholderResponse;
}
- public Response Get(string key, DownstreamRequest request)
- {
- var placeholderResponse = _placeholders.Get(key, request);
+ return GetFromConfig(CleanKey(key));
+ }
- if (!placeholderResponse.IsError)
- {
- return placeholderResponse;
- }
+ public Response Get(string key, DownstreamRequest request)
+ {
+ var placeholderResponse = _placeholders.Get(key, request);
- return GetFromConfig(CleanKey(key));
+ if (!placeholderResponse.IsError)
+ {
+ return placeholderResponse;
}
- public Response Add(string key, Func> func)
- => _placeholders.Add(key, func);
+ return GetFromConfig(CleanKey(key));
+ }
+
+ public Response Add(string key, Func> func)
+ => _placeholders.Add(key, func);
- public Response Remove(string key)
- => _placeholders.Remove(key);
+ public Response Remove(string key)
+ => _placeholders.Remove(key);
- private static string CleanKey(string key)
- => Regex.Replace(key, @"[{}]", string.Empty, RegexOptions.None);
+#if NET7_0_OR_GREATER
+ [GeneratedRegex(@"[{}]", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
+ private static partial Regex Regex();
+#else
+ private static readonly Regex _regex = RegexGlobal.New(@"[{}]");
+ private static Regex Regex() => _regex;
+#endif
+ private static string CleanKey(string key)
+ => Regex().Replace(key, string.Empty);
- private Response GetFromConfig(string key)
- {
- var valueFromConfig = _configuration[key];
- return valueFromConfig == null
- ? new ErrorResponse(new CouldNotFindPlaceholderError(key))
- : new OkResponse(valueFromConfig);
- }
+ private Response GetFromConfig(string key)
+ {
+ var valueFromConfig = _configuration[key];
+ return valueFromConfig == null
+ ? new ErrorResponse(new CouldNotFindPlaceholderError(key))
+ : new OkResponse(valueFromConfig);
}
}
diff --git a/src/Ocelot/Infrastructure/RegexGlobal.cs b/src/Ocelot/Infrastructure/RegexGlobal.cs
new file mode 100644
index 000000000..a9847040e
--- /dev/null
+++ b/src/Ocelot/Infrastructure/RegexGlobal.cs
@@ -0,0 +1,39 @@
+using Ocelot.DependencyInjection;
+
+namespace Ocelot.Infrastructure;
+
+public static class RegexGlobal
+{
+ static RegexGlobal()
+ {
+ RegexCacheSize = DefaultRegexCacheSize;
+ DefaultMatchTimeout = TimeSpan.FromMilliseconds(DefaultMatchTimeoutMilliseconds);
+ AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", DefaultMatchTimeout);
+ }
+
+ /// Default value of the property.
+ public const int DefaultRegexCacheSize = 100;
+
+ /// Gets or sets the global value to assign to the property.
+ /// Ocelot forcibly assigns this value during app startup, see class.
+ ///
+ /// Default value is 100 aka .
+ /// Default .NET value of is 15.
+ /// An value.
+ public static int RegexCacheSize { get; set; }
+
+ /// Default value for the attribute and the constructors.
+ public const int DefaultMatchTimeoutMilliseconds = 100;
+
+ /// Default match timeout for the constructors.
+ /// Default value is 100 ms aka .
+ /// A value.
+ public static TimeSpan DefaultMatchTimeout { get; set; }
+
+ public static Regex New(string pattern)
+ => new(pattern, RegexOptions.Compiled, DefaultMatchTimeout);
+ public static Regex New(string pattern, RegexOptions options)
+ => new(pattern, options | RegexOptions.Compiled, DefaultMatchTimeout);
+ public static Regex New(string pattern, RegexOptions options, TimeSpan matchTimeout)
+ => new(pattern, options | RegexOptions.Compiled, matchTimeout);
+}
diff --git a/src/Ocelot/Values/UpstreamHeaderTemplate.cs b/src/Ocelot/Values/UpstreamHeaderTemplate.cs
index 3151fbdf8..df411f96f 100644
--- a/src/Ocelot/Values/UpstreamHeaderTemplate.cs
+++ b/src/Ocelot/Values/UpstreamHeaderTemplate.cs
@@ -1,4 +1,6 @@
-namespace Ocelot.Values;
+using Ocelot.Infrastructure;
+
+namespace Ocelot.Values;
///
/// Upstream template properties of headers and their regular expression.
@@ -14,6 +16,6 @@ public UpstreamHeaderTemplate(string template, string originalValue)
{
Template = template;
OriginalValue = originalValue;
- Pattern = new Regex(template ?? "$^", RegexOptions.Compiled | RegexOptions.Singleline);
+ Pattern = RegexGlobal.New(template ?? "$^", RegexOptions.Singleline);
}
}
diff --git a/src/Ocelot/Values/UpstreamPathTemplate.cs b/src/Ocelot/Values/UpstreamPathTemplate.cs
index 7bddce01e..e518d42e6 100644
--- a/src/Ocelot/Values/UpstreamPathTemplate.cs
+++ b/src/Ocelot/Values/UpstreamPathTemplate.cs
@@ -1,26 +1,38 @@
-namespace Ocelot.Values
+using Ocelot.Infrastructure;
+
+namespace Ocelot.Values;
+
+/// The model to keep data of upstream path.
+public partial class UpstreamPathTemplate
{
- public class UpstreamPathTemplate
+#if NET7_0_OR_GREATER
+ [GeneratedRegex("$^", RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
+ private static partial Regex RegexNoTemplate();
+#else
+ private static readonly Regex _regexNoTemplate = RegexGlobal.New("$^", RegexOptions.Singleline);
+ private static Regex RegexNoTemplate() => _regexNoTemplate;
+#endif
+ private static readonly ConcurrentDictionary _regex = new();
+
+ public UpstreamPathTemplate(string template, int priority, bool containsQueryString, string originalValue)
{
- public UpstreamPathTemplate(string template, int priority, bool containsQueryString, string originalValue)
- {
- Template = template;
- Priority = priority;
- ContainsQueryString = containsQueryString;
- OriginalValue = originalValue;
- Pattern = template == null ?
- new Regex("$^", RegexOptions.Compiled | RegexOptions.Singleline) :
- new Regex(template, RegexOptions.Compiled | RegexOptions.Singleline);
- }
+ Template = template;
+ Priority = priority;
+ ContainsQueryString = containsQueryString;
+ OriginalValue = originalValue;
+ Pattern = template == null ? RegexNoTemplate() :
+ _regex.AddOrUpdate(template,
+ RegexGlobal.New(template, RegexOptions.Singleline),
+ (key, oldValue) => oldValue);
+ }
- public string Template { get; }
+ public string Template { get; }
- public int Priority { get; }
+ public int Priority { get; }
- public bool ContainsQueryString { get; }
+ public bool ContainsQueryString { get; }
- public string OriginalValue { get; }
+ public string OriginalValue { get; }
- public Regex Pattern { get; }
- }
+ public Regex Pattern { get; }
}
diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs
index 80d56628b..650f32602 100644
--- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs
+++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs
@@ -7,6 +7,7 @@
using Ocelot.Configuration;
using Ocelot.Configuration.File;
using Ocelot.DependencyInjection;
+using Ocelot.Infrastructure;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Logging;
using Ocelot.Provider.Consul;
@@ -594,10 +595,10 @@ private void GivenIResetCounters()
private void GivenTheServiceNodesAreRegisteredWithConsul(params Node[] nodes) => _consulNodes.AddRange(nodes);
#if NET7_0_OR_GREATER
- [GeneratedRegex("/v1/health/service/(?[^/]+)")]
+ [GeneratedRegex("/v1/health/service/(?[^/]+)", RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
private static partial Regex ServiceNameRegex();
#else
- private static readonly Regex ServiceNameRegexVar = new("/v1/health/service/(?[^/]+)");
+ private static readonly Regex ServiceNameRegexVar = RegexGlobal.New("/v1/health/service/(?[^/]+)", RegexOptions.Singleline);
private static Regex ServiceNameRegex() => ServiceNameRegexVar;
#endif
private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url)