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)