// Note this performs all validation steps without short circuiting in order to report all possible errors. public async Task <bool> ValidateRouteAsync(ParsedRoute route, IConfigErrorReporter errorReporter) { _ = route ?? throw new ArgumentNullException(nameof(route)); _ = errorReporter ?? throw new ArgumentNullException(nameof(errorReporter)); var success = true; if (string.IsNullOrEmpty(route.RouteId)) { errorReporter.ReportError(ConfigErrors.ParsedRouteMissingId, route.RouteId, $"Route has no {nameof(route.RouteId)}."); success = false; } if (string.IsNullOrEmpty(route.Host) && string.IsNullOrEmpty(route.Path)) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleHasNoMatchers, route.RouteId, $"Route requires {nameof(route.Host)} or {nameof(route.Path)} specified. Set the Path to `/{{**catchall}}` to match all requests."); success = false; } success &= ValidateHost(route.Host, route.RouteId, errorReporter); success &= ValidatePath(route.Path, route.RouteId, errorReporter); success &= ValidateMethods(route.Methods, route.RouteId, errorReporter); success &= _transformBuilder.Validate(route.Transforms, route.RouteId, errorReporter); success &= await ValidateAuthorizationPolicyAsync(route.AuthorizationPolicy, route.RouteId, errorReporter); success &= await ValidateCorsPolicyAsync(route.CorsPolicy, route.RouteId, errorReporter); return(success); }
public async Task <IDictionary <string, Cluster> > GetClustersAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation) { var clusters = await _clustersRepo.GetClustersAsync(cancellation) ?? new Dictionary <string, Cluster>(StringComparer.Ordinal); var configuredClusters = new Dictionary <string, Cluster>(StringComparer.Ordinal); // The IClustersRepo provides a fresh snapshot that we need to reconfigure each time. foreach (var(id, cluster) in clusters) { try { if (id != cluster.Id) { errorReporter.ReportError(ConfigErrors.ConfigBuilderClusterIdMismatch, id, $"The cluster Id '{cluster.Id}' and its lookup key '{id}' do not match."); continue; } foreach (var filter in _filters) { await filter.ConfigureClusterAsync(cluster, cancellation); } ValidateSessionAffinity(errorReporter, id, cluster); configuredClusters[id] = cluster; } catch (Exception ex) { errorReporter.ReportError(ConfigErrors.ConfigBuilderClusterException, id, "An exception was thrown from the configuration callbacks.", ex); } } return(configuredClusters); }
public async Task <IDictionary <string, Backend> > GetBackendsAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation) { var backends = await _backendsRepo.GetBackendsAsync(cancellation) ?? new Dictionary <string, Backend>(StringComparer.Ordinal); var configuredBackends = new Dictionary <string, Backend>(StringComparer.Ordinal); // The IBackendsRepo provides a fresh snapshot that we need to reconfigure each time. foreach (var(id, backend) in backends) { try { if (id != backend.Id) { errorReporter.ReportError(ConfigErrors.ConfigBuilderBackendIdMismatch, id, $"The backend Id '{backend.Id}' and its lookup key '{id}' do not match."); continue; } foreach (var filter in _filters) { await filter.ConfigureBackendAsync(backend, cancellation); } ValidateSessionAffinity(errorReporter, id, backend); configuredBackends[id] = backend; } catch (Exception ex) { errorReporter.ReportError(ConfigErrors.ConfigBuilderBackendException, id, "An exception was thrown from the configuration callbacks.", ex); } } return(configuredBackends); }
public async Task <IDictionary <string, Backend> > GetBackendsAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation) { var backends = await _backendsRepo.GetBackendsAsync(cancellation) ?? new Dictionary <string, Backend>(StringComparer.Ordinal); var configuredBackends = new Dictionary <string, Backend>(StringComparer.Ordinal); // The IBackendsRepo provides a fresh snapshot that we need to reconfigure each time. foreach (var(id, backend) in backends) { try { foreach (var filter in _filters) { await filter.ConfigureBackendAsync(id, backend, cancellation); } configuredBackends[id] = backend; } catch (Exception ex) { errorReporter.ReportError(ConfigErrors.ConfigBuilderBackendException, id, "An exception was thrown from the configuration callbacks.", ex); } } return(configuredBackends); }
// Note this performs all validation steps without short circuiting in order to report all possible errors. public bool ValidateRoute(ParsedRoute route, IConfigErrorReporter errorReporter) { Contracts.CheckValue(route, nameof(route)); Contracts.CheckValue(errorReporter, nameof(errorReporter)); var success = true; if (string.IsNullOrEmpty(route.RouteId)) { errorReporter.ReportError(ConfigErrors.ParsedRouteMissingId, route.RouteId, $"Route has no {nameof(route.RouteId)}."); success = false; } if (string.IsNullOrEmpty(route.Host) && string.IsNullOrEmpty(route.Path)) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleHasNoMatchers, route.RouteId, $"Route requires {nameof(route.Host)} or {nameof(route.Path)} specified. Set the Path to `/{{**catchall}}` to match all requests."); success = false; } success &= ValidateHost(route.Host, route.RouteId, errorReporter); success &= ValidatePath(route.Path, route.RouteId, errorReporter); success &= ValidateMethods(route.Methods, route.RouteId, errorReporter); success &= _transformBuilder.Validate(route.Transforms, route.RouteId, errorReporter); return(success); }
private void ValidateSessionAffinity(IConfigErrorReporter errorReporter, string id, Cluster cluster) { if (cluster.SessionAffinity == null || !cluster.SessionAffinity.Enabled) { // Session affinity is disabled return; } if (string.IsNullOrEmpty(cluster.SessionAffinity.Mode)) { cluster.SessionAffinity.Mode = SessionAffinityConstants.Modes.Cookie; } var affinityMode = cluster.SessionAffinity.Mode; if (!_sessionAffinityProviders.ContainsKey(affinityMode)) { errorReporter.ReportError(ConfigErrors.ConfigBuilderClusterNoProviderFoundForSessionAffinityMode, id, $"No matching {nameof(ISessionAffinityProvider)} found for the session affinity mode {affinityMode} set on the cluster {cluster.Id}."); } if (string.IsNullOrEmpty(cluster.SessionAffinity.FailurePolicy)) { cluster.SessionAffinity.FailurePolicy = SessionAffinityConstants.AffinityFailurePolicies.Redistribute; } var affinityFailurePolicy = cluster.SessionAffinity.FailurePolicy; if (!_affinityFailurePolicies.ContainsKey(affinityFailurePolicy)) { errorReporter.ReportError(ConfigErrors.ConfigBuilderClusterNoAffinityFailurePolicyFoundForSpecifiedName, id, $"No matching {nameof(IAffinityFailurePolicy)} found for the affinity failure policy name {affinityFailurePolicy} set on the cluster {cluster.Id}."); } }
private async Task <IList <ParsedRoute> > GetRoutesAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation) { var routes = await _routesRepo.GetRoutesAsync(cancellation); var seenRouteIds = new HashSet <string>(); var sortedRoutes = new SortedList <(int, string), ParsedRoute>(routes?.Count ?? 0); if (routes == null) { return(sortedRoutes.Values); } foreach (var route in routes) { if (seenRouteIds.Contains(route.RouteId)) { errorReporter.ReportError(ConfigErrors.RouteDuplicateId, route.RouteId, $"Duplicate route '{route.RouteId}'."); continue; } try { foreach (var filter in _filters) { await filter.ConfigureRouteAsync(route, cancellation); } } catch (Exception ex) { errorReporter.ReportError(ConfigErrors.ConfigBuilderClusterException, route.RouteId, "An exception was thrown from the configuration callbacks.", ex); continue; } var parsedRoute = new ParsedRoute { RouteId = route.RouteId, Methods = route.Match.Methods, Host = route.Match.Host, Path = route.Match.Path, Priority = route.Priority, ClusterId = route.ClusterId, AuthorizationPolicy = route.AuthorizationPolicy, CorsPolicy = route.CorsPolicy, Metadata = route.Metadata, Transforms = route.Transforms, }; if (!await _parsedRouteValidator.ValidateRouteAsync(parsedRoute, errorReporter)) { // parsedRouteValidator already reported error message continue; } sortedRoutes.Add((parsedRoute.Priority ?? 0, parsedRoute.RouteId), parsedRoute); } return(sortedRoutes.Values); }
/// <inheritdoc/> public async Task ApplyConfigurationsAsync(IConfigErrorReporter configErrorReporter, CancellationToken cancellation) { if (configErrorReporter == null) { throw new ArgumentNullException(nameof(configErrorReporter)); } var config = await _configBuilder.BuildConfigAsync(configErrorReporter, cancellation); UpdateRuntimeClusters(config); UpdateRuntimeRoutes(config); }
public async Task <DynamicConfigRoot> BuildConfigAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation) { var clusters = await GetClustersAsync(errorReporter, cancellation); var routes = await GetRoutesAsync(errorReporter, cancellation); var config = new DynamicConfigRoot { Clusters = clusters, Routes = routes, }; return(config); }
public async Task <Result <DynamicConfigRoot> > BuildConfigAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation) { Contracts.CheckValue(errorReporter, nameof(errorReporter)); var backends = await _backendsRepo.GetBackendsAsync(cancellation) ?? new Dictionary <string, Backend>(StringComparer.Ordinal); var routes = await GetRoutesAsync(errorReporter, cancellation); var config = new DynamicConfigRoot { Backends = backends, Routes = routes, }; return(Result.Success(config)); }
private static bool ValidateHost(string host, string routeId, IConfigErrorReporter errorReporter) { // Host is optional when Path is specified if (string.IsNullOrEmpty(host)) { return(true); } if (!_hostNameRegex.IsMatch(host)) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidMatcher, routeId, $"Invalid host name '{host}'"); return(false); } return(true); }
public async Task <Result <DynamicConfigRoot> > BuildConfigAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation) { Contracts.CheckValue(errorReporter, nameof(errorReporter)); var backends = await GetBackendsAsync(errorReporter, cancellation); var routes = await GetRoutesAsync(errorReporter, cancellation); var config = new DynamicConfigRoot { Backends = backends, Routes = routes, }; return(Result.Success(config)); }
private static bool ValidateHost(string host, string routeId, IConfigErrorReporter errorReporter) { // TODO: Why is Host required? I'd only expect Host OR Path to be required, with Path being the more common usage. if (string.IsNullOrEmpty(host)) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleMissingHostMatcher, routeId, $"Route '{routeId}' is missing required field 'Host'."); return(false); } if (!_hostNameRegex.IsMatch(host)) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidMatcher, routeId, $"Invalid host name '{host}'"); return(false); } return(true); }
// Note this performs all validation steps without short circuiting in order to report all possible errors. public bool ValidateRoute(ParsedRoute route, IConfigErrorReporter errorReporter) { Contracts.CheckValue(route, nameof(route)); Contracts.CheckValue(errorReporter, nameof(errorReporter)); var success = true; if (string.IsNullOrEmpty(route.RouteId)) { errorReporter.ReportError(ConfigErrors.ParsedRouteMissingId, route.RouteId, $"Route has no {nameof(route.RouteId)}."); success = false; } success &= ValidateHost(route.Host, route.RouteId, errorReporter); success &= ValidatePath(route.Path, route.RouteId, errorReporter); success &= ValidateMethods(route.Methods, route.RouteId, errorReporter); return(success); }
private static bool ValidatePath(string path, string routeId, IConfigErrorReporter errorReporter) { // Path is optional when Host is specified if (string.IsNullOrEmpty(path)) { return(true); } try { RoutePatternFactory.Parse(path); } catch (RoutePatternException ex) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidMatcher, routeId, $"Invalid path pattern '{path}'", ex); return(false); } return(true); }
/// <inheritdoc/> public async Task <bool> ApplyConfigurationsAsync(IConfigErrorReporter configErrorReporter, CancellationToken cancellation) { if (configErrorReporter == null) { throw new ArgumentNullException(nameof(configErrorReporter)); } var configResult = await _configBuilder.BuildConfigAsync(configErrorReporter, cancellation); if (!configResult.IsSuccess) { return(false); } var config = configResult.Value; UpdateRuntimeBackends(config); UpdateRuntimeRoutes(config); return(true); }
private async Task <IList <ParsedRoute> > GetRoutesAsync(IConfigErrorReporter errorReporter, CancellationToken cancellation) { var routes = await _routesRepo.GetRoutesAsync(cancellation); var seenRouteIds = new HashSet <string>(); var sortedRoutes = new SortedList <(int, string), ParsedRoute>(routes?.Count ?? 0); if (routes != null) { foreach (var route in routes) { if (seenRouteIds.Contains(route.RouteId)) { errorReporter.ReportError(ConfigErrors.RouteDuplicateId, route.RouteId, $"Duplicate route '{route.RouteId}'."); continue; } var parsedRoute = new ParsedRoute { RouteId = route.RouteId, Methods = route.Match.Methods, Host = route.Match.Host, Path = route.Match.Path, Priority = route.Priority, BackendId = route.BackendId, Metadata = route.Metadata, }; if (!_parsedRouteValidator.ValidateRoute(parsedRoute, errorReporter)) { // parsedRouteValidator already reported error message continue; } sortedRoutes.Add((parsedRoute.Priority ?? 0, parsedRoute.RouteId), parsedRoute); } } return(sortedRoutes.Values); }
private static bool ValidateMethods(IReadOnlyList <string> methods, string routeId, IConfigErrorReporter errorReporter) { // Methods are optional if (methods == null) { return(true); } var seenMethods = new HashSet <string>(StringComparer.OrdinalIgnoreCase); foreach (var method in methods) { if (!seenMethods.Add(method)) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidMatcher, routeId, $"Duplicate verb '{method}'"); return(false); } if (!_validMethods.Contains(method)) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidMatcher, routeId, $"Unsupported verb '{method}'"); return(false); } } return(true); }
private async Task <bool> ValidateCorsPolicyAsync(string corsPolicyName, string routeId, IConfigErrorReporter errorReporter) { if (string.IsNullOrEmpty(corsPolicyName)) { return(true); } if (string.Equals(CorsConstants.Default, corsPolicyName, StringComparison.OrdinalIgnoreCase)) { return(true); } if (string.Equals(CorsConstants.Disable, corsPolicyName, StringComparison.OrdinalIgnoreCase)) { return(true); } try { var dummyHttpContext = new DefaultHttpContext(); var policy = await _corsPolicyProvider.GetPolicyAsync(dummyHttpContext, corsPolicyName); if (policy == null) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidCorsPolicy, routeId, $"Cors policy '{corsPolicyName}' not found."); return(false); } } catch (Exception ex) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidCorsPolicy, routeId, $"Unable to retrieve the cors policy '{corsPolicyName}'", ex); return(false); } return(true); }
private bool TryCheckTooManyParameters(IDictionary <string, string> rawTransform, string routeId, int expected, IConfigErrorReporter errorReporter) { if (rawTransform.Count > expected) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"The transform contains more parameters than the expected {expected}: {string.Join(';', rawTransform.Keys)}."); return(false); } return(true); }
private async Task <bool> ValidateAuthorizationPolicyAsync(string authorizationPolicyName, string routeId, IConfigErrorReporter errorReporter) { if (string.IsNullOrEmpty(authorizationPolicyName)) { return(true); } if (string.Equals(AuthorizationConstants.Default, authorizationPolicyName, StringComparison.OrdinalIgnoreCase)) { return(true); } try { var policy = await _authorizationPolicyProvider.GetPolicyAsync(authorizationPolicyName); if (policy == null) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidAuthorizationPolicy, routeId, $"Authorization policy '{authorizationPolicyName}' not found."); return(false); } } catch (Exception ex) { errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidAuthorizationPolicy, routeId, $"Unable to retrieve the authorization policy '{authorizationPolicyName}'", ex); return(false); } return(true); }
/// <inheritdoc/> public bool Validate(IList <IDictionary <string, string> > rawTransforms, string routeId, IConfigErrorReporter errorReporter) { if (routeId is null) { throw new ArgumentNullException(nameof(routeId)); } if (errorReporter is null) { throw new ArgumentNullException(nameof(errorReporter)); } var success = true; if (rawTransforms == null || rawTransforms.Count == 0) { return(success); } foreach (var rawTransform in rawTransforms) { if (rawTransform.TryGetValue("PathSet", out var pathSet)) { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 1, errorReporter); } else if (rawTransform.TryGetValue("PathPrefix", out var pathPrefix)) { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 1, errorReporter); } else if (rawTransform.TryGetValue("PathRemovePrefix", out var pathRemovePrefix)) { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 1, errorReporter); } else if (rawTransform.TryGetValue("PathPattern", out var pathPattern)) { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 1, errorReporter); // TODO: Validate the pattern format. Does it build? } else if (rawTransform.TryGetValue("RequestHeadersCopy", out var copyHeaders)) { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 1, errorReporter); if (!string.Equals("True", copyHeaders, StringComparison.OrdinalIgnoreCase) && !string.Equals("False", copyHeaders, StringComparison.OrdinalIgnoreCase)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected value for RequestHeaderCopy: {copyHeaders}. Expected 'true' or 'false'"); success = false; } } else if (rawTransform.TryGetValue("RequestHeaderOriginalHost", out var originalHost)) { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 1, errorReporter); if (!string.Equals("True", originalHost, StringComparison.OrdinalIgnoreCase) && !string.Equals("False", originalHost, StringComparison.OrdinalIgnoreCase)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected value for RequestHeaderOriginalHost: {originalHost}. Expected 'true' or 'false'"); success = false; } } else if (rawTransform.TryGetValue("RequestHeader", out var headerName)) { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 2, errorReporter); if (!rawTransform.TryGetValue("Set", out var _) && !rawTransform.TryGetValue("Append", out var _)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected parameters for RequestHeader: {string.Join(';', rawTransform.Keys)}. Expected 'Set' or 'Append'"); success = false; } } else if (rawTransform.TryGetValue("ResponseHeader", out var _)) { if (rawTransform.TryGetValue("When", out var whenValue)) { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 3, errorReporter); if (!string.Equals("Always", whenValue, StringComparison.OrdinalIgnoreCase) && !string.Equals("Success", whenValue, StringComparison.OrdinalIgnoreCase)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected value for ResponseHeader:When: {whenValue}. Expected 'Always' or 'Success'"); success = false; } } else { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 2, errorReporter); } if (!rawTransform.TryGetValue("Set", out var _) && !rawTransform.TryGetValue("Append", out var _)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected parameters for ResponseHeader: {string.Join(';', rawTransform.Keys)}. Expected 'Set' or 'Append'"); success = false; } } else if (rawTransform.TryGetValue("ResponseTrailer", out var _)) { if (rawTransform.TryGetValue("When", out var whenValue)) { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 3, errorReporter); if (!string.Equals("Always", whenValue, StringComparison.OrdinalIgnoreCase) && !string.Equals("Success", whenValue, StringComparison.OrdinalIgnoreCase)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected value for ResponseTrailer:When: {whenValue}. Expected 'Always' or 'Success'"); success = false; } } else { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 2, errorReporter); } if (!rawTransform.TryGetValue("Set", out var _) && !rawTransform.TryGetValue("Append", out var _)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected parameters for ResponseTrailer: {string.Join(';', rawTransform.Keys)}. Expected 'Set' or 'Append'"); success = false; } } else if (rawTransform.TryGetValue("X-Forwarded", out var xforwardedHeaders)) { var expected = 1; if (rawTransform.TryGetValue("Append", out var appendValue)) { expected++; if (!string.Equals("True", appendValue, StringComparison.OrdinalIgnoreCase) && !string.Equals("False", appendValue, StringComparison.OrdinalIgnoreCase)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected value for X-Forwarded:Append: {appendValue}. Expected 'true' or 'false'"); success = false; } } if (rawTransform.TryGetValue("Prefix", out var _)) { expected++; } success &= TryCheckTooManyParameters(rawTransform, routeId, expected, errorReporter); // for, host, proto, PathBase var tokens = xforwardedHeaders.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); foreach (var token in tokens) { if (!string.Equals(token, "For", StringComparison.OrdinalIgnoreCase) && !string.Equals(token, "Host", StringComparison.OrdinalIgnoreCase) && !string.Equals(token, "Proto", StringComparison.OrdinalIgnoreCase) && !string.Equals(token, "PathBase", StringComparison.OrdinalIgnoreCase)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected value for X-Forwarded: {token}. Expected 'for', 'host', 'proto', or 'PathBase'"); success = false; } } } else if (rawTransform.TryGetValue("Forwarded", out var forwardedHeader)) { var expected = 1; if (rawTransform.TryGetValue("Append", out var appendValue)) { expected++; if (!string.Equals("True", appendValue, StringComparison.OrdinalIgnoreCase) && !string.Equals("False", appendValue, StringComparison.OrdinalIgnoreCase)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected value for Forwarded:Append: {appendValue}. Expected 'true' or 'false'"); success = false; } } var enumValues = "Random,RandomAndPort,Unknown,UnknownAndPort,Ip,IpAndPort"; if (rawTransform.TryGetValue("ForFormat", out var forFormat)) { expected++; if (!Enum.TryParse <RequestHeaderForwardedTransform.NodeFormat>(forFormat, ignoreCase: true, out var _)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected value for Forwarded:ForFormat: {forFormat}. Expected: {enumValues}"); success = false; } } if (rawTransform.TryGetValue("ByFormat", out var byFormat)) { expected++; if (!Enum.TryParse <RequestHeaderForwardedTransform.NodeFormat>(byFormat, ignoreCase: true, out var _)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected value for Forwarded:ByFormat: {byFormat}. Expected: {enumValues}"); success = false; } } success &= TryCheckTooManyParameters(rawTransform, routeId, expected, errorReporter); // for, host, proto, by var tokens = forwardedHeader.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); foreach (var token in tokens) { if (!string.Equals(token, "By", StringComparison.OrdinalIgnoreCase) && !string.Equals(token, "Host", StringComparison.OrdinalIgnoreCase) && !string.Equals(token, "Proto", StringComparison.OrdinalIgnoreCase) && !string.Equals(token, "For", StringComparison.OrdinalIgnoreCase)) { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unexpected value for X-Forwarded: {token}. Expected 'for', 'host', 'proto', or 'by'"); success = false; } } } else if (rawTransform.TryGetValue("ClientCert", out var clientCertHeader)) { success &= TryCheckTooManyParameters(rawTransform, routeId, expected: 1, errorReporter); } else { errorReporter.ReportError(ConfigErrors.TransformInvalid, routeId, $"Unknown transform: {string.Join(';', rawTransform.Keys)}"); success = false; } } return(success); }