Пример #1
0
        // 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);
        }
Пример #2
0
        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);
        }
Пример #3
0
        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);
        }
Пример #4
0
        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);
        }
Пример #5
0
        // 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);
        }
Пример #6
0
        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);
        }
Пример #9
0
        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);
        }
Пример #10
0
        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));
        }
Пример #11
0
        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);
        }
Пример #12
0
        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));
        }
Пример #13
0
        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);
        }
Пример #14
0
        // 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);
        }
Пример #15
0
        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);
        }
Пример #17
0
        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);
        }
Пример #18
0
        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);
        }
Пример #19
0
        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);
        }
Пример #20
0
        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);
        }
Пример #21
0
        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);
        }
Пример #22
0
        /// <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);
        }