请求和响应转换扩展性

介绍

代理请求时,通常修改请求或响应的各个部分,以适应目标服务器的要求或流出其他数据,例如客户端的原始 IP 地址。 此过程通过Transforms实现。 为应用程序全局定义转换类型,然后各个路由提供参数以启用和配置这些转换。 原始请求对象不会由这些转换修改,只修改代理请求。

YARP 包括一组可供使用的内置请求和响应转换。 有关详细信息,请参阅 YARP 请求和响应转换。 如果这些转换不够,则可以添加自定义转换。

RequestTransform

所有请求转换都必须派生自抽象基类 RequestTransform。 这些可以自由修改代理 HttpRequestMessage。 避免读取或修改请求正文,因为这可能会中断代理流。 另请考虑为 TransformBuilderContext 添加参数化扩展方法,以提高可发现性和易用性。

请求转换可能会有条件地生成即时响应,例如错误条件。 这样可以防止运行任何剩余的转换并防止代理请求。 这是通过将 HttpResponse.StatusCode 设置为不等于200的值,调用 HttpResponse.StartAsync(),或者写入 HttpResponse.BodyBodyWriter 来指示。

AddRequestTransform 是一种 TransformBuilderContext 扩展方法,它将请求转换定义为 Func<RequestTransformContext, ValueTask>。 这样就可以创建自定义请求转换,而无需实现 RequestTransform 派生类。

ResponseTransform

所有响应转换都必须派生自抽象基类 ResponseTransform。 这些可以自由修改客户端 HttpResponse。 避免读取或修改响应正文,因为这可能会中断代理流。 另请考虑添加参数化扩展方法 TransformBuilderContext ,以便于可发现性和易于使用。

AddResponseTransform 是一种 TransformBuilderContext 扩展方法,能够将响应转换表述为 Func<ResponseTransformContext, ValueTask>。 这样就可以创建自定义响应转换,而无需实现 ResponseTransform 派生类。

ResponseTrailersTransform

所有响应尾部转换器都必须派生自抽象基类 ResponseTrailersTransform。 这些可以自由修改客户端 HttpResponse 尾部。 这些程序在响应正文之后执行,不应尝试修改响应标头或正文。 另请考虑添加参数化扩展方法 TransformBuilderContext ,以便于可发现性和易于使用。

AddResponseTrailersTransform 是一种 TransformBuilderContext 扩展方法,用于将响应预告片转换定义为 . Func<ResponseTrailersTransformContext, ValueTask>. 这允许创建自定义响应尾部转换,而无需实现 ResponseTrailersTransform 派生类。

请求正文转换

YARP 不提供任何用于修改请求正文的内置转换。 但是,可以通过自定义转换来修改正文。

请注意修改了哪些类型的请求、数据缓存量、实施超时、解析不受信任的输入,以及更新与正文相关的标头,如 Content-Length

下面的示例使用简单、低效的缓冲来转换请求。 更高效的实现将把 HttpContext.Request.Body 包装并替换为一个流,该流在数据从客户端代理到服务器时执行所需的修改。 这还需要删除 Content-Length 头,因为无法提前知道最终长度。

此示例需要 YARP 1.1,请参阅 https://github.com/microsoft/reverse-proxy/pull/1569

.AddTransforms(context =>
{
    context.AddRequestTransform(async requestContext =>
    {
        using var reader =
            new StreamReader(requestContext.HttpContext.Request.Body);
        // TODO: size limits, timeouts
        var body = await reader.ReadToEndAsync();
        if (!string.IsNullOrEmpty(body))
        {
            body = body.Replace("Alpha", "Charlie");
            var bytes = Encoding.UTF8.GetBytes(body);
            // Change Content-Length to match the modified body, or remove it
            requestContext.HttpContext.Request.Body = new MemoryStream(bytes);
            // Request headers are copied before transforms are invoked, update any
            // needed headers on the ProxyRequest
            requestContext.ProxyRequest.Content.Headers.ContentLength =
                bytes.Length;
        }
    });
});

自定义转换只能修改请求正文(如果已存在)。 它们无法向没有正文的请求中添加新的正文(例如,没有正文的 POST 请求或 GET 请求)。 如果需要为特定的 HTTP 方法和路由添加正文,您必须在 YARP 之前运行的中间件中完成此操作,而不是在转换中进行。

以下中间件演示如何向没有正文的请求添加正文:

public class AddRequestBodyMiddleware
{
    private readonly RequestDelegate _next;

    public AddRequestBodyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Only modify specific route and method
        if (context.Request.Method == HttpMethods.Get &&
            context.Request.Path == "/special-route")
        {
            var bodyContent = "key=value";
            var bodyBytes = Encoding.UTF8.GetBytes(bodyContent);

            // Create a new request body
            context.Request.Body = new MemoryStream(bodyBytes);
            context.Request.ContentLength = bodyBytes.Length;

            // Replace IHttpRequestBodyDetectionFeature so YARP knows
            // a body is present
            context.Features.Set<IHttpRequestBodyDetectionFeature>(
                new CustomBodyDetectionFeature());
        }

        await _next(context);
    }

    // Helper class to indicate the request can have a body
    private class CustomBodyDetectionFeature : IHttpRequestBodyDetectionFeature
    {
        public bool CanHaveBody => true;
    }
}

注释

在中间件中,可以通过 context.GetRouteModel().Config.RouteId 为特定的 YARP 路由有条件地应用此逻辑。

响应正文转换

YARP 不提供任何用于修改响应正文的内置转换。 但是,可以通过自定义转换来修改正文。

请注意修改了哪些类型的响应、数据缓存量、实施超时、解析不受信任的输入,以及更新与正文相关的标头,如 Content-Length。 在修改内容之前,可能需要解压缩内容,如 Content-Encoding 标头所指示,然后重新压缩或删除标头。

下面的示例使用简单、低效的缓冲来转换响应。 更高效的实现会将 ReadAsStreamAsync() 返回的流与执行所需修改的流整合在一起,因为数据会从客户端代理到服务器。 这还需要删除 Content-Length 头,因为无法提前知道最终长度。

.AddTransforms(context =>
{
    context.AddResponseTransform(async responseContext =>
    {
        var stream =
            await responseContext.ProxyResponse.Content.ReadAsStreamAsync();
        using var reader = new StreamReader(stream);
        // TODO: size limits, timeouts
        var body = await reader.ReadToEndAsync();

        if (!string.IsNullOrEmpty(body))
        {
            responseContext.SuppressResponseBody = true;

            body = body.Replace("Bravo", "Charlie");
            var bytes = Encoding.UTF8.GetBytes(body);
            // Change Content-Length to match the modified body, or remove it
            responseContext.HttpContext.Response.ContentLength = bytes.Length;
            // Response headers are copied before transforms are invoked, update
            // any needed headers on the HttpContext.Response
            await responseContext.HttpContext.Response.Body.WriteAsync(bytes);
        }
    });
});

ITransformProvider

ITransformProvider 提供上述描述的 AddTransforms 功能,以及 DI 集成和验证支持。

通过调用 ITransformProvider 可以在 DI 中注册 AddTransforms。 可以注册多个 ITransformProvider 实现,所有实现都将运行。

ITransformProvider 有两种方法,ValidateApplyValidate 让你有机会检查配置转换(如自定义元数据)所需的任何参数的路由,并返回上下文中的验证错误(如果需要的值缺失或无效)。 Apply 方法提供的功能与上述 AddTransform 方法提供的功能相同,并且此方法会按路由添加和配置转换。

services.AddReverseProxy()
    .LoadFromConfig(_configuration.GetSection("ReverseProxy"))
    .AddTransforms<MyTransformProvider>();
internal class MyTransformProvider : ITransformProvider
{
    public void ValidateRoute(TransformRouteValidationContext context)
    {
        // Check all routes for a custom property and validate the associated
        // transform data
        if (context.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ??
            false)
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomMetadata value is required"));
            }
        }
    }

    public void ValidateCluster(TransformClusterValidationContext context)
    {
        // Check all clusters for a custom property and validate the associated
        // transform data.
        if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out var value)
            ?? false)
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomMetadata value is required"));
            }
        }
    }

    public void Apply(TransformBuilderContext transformBuildContext)
    {
        // Check all routes for a custom property and add the associated transform.
        if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata",
            out var value) ?? false)
            || (transformBuildContext.Cluster?.Metadata?.TryGetValue(
            "CustomMetadata", out value) ?? false))
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException(
                    "A non-empty CustomMetadata value is required");
            }

            transformBuildContext.AddRequestTransform(transformContext =>
            {
                transformContext.ProxyRequest.Options.Set(
                    new HttpRequestOptionsKey<string>("CustomMetadata"), value);

                return default;
            });
        }
    }
}

ITransformFactory

想要将其自定义转换与 Transforms 配置部分集成的开发人员可以实现一个 ITransformFactory。 这应该使用 AddTransformFactory<T>() 方法在 DI 中进行注册。 可以注册多个工厂,而且所有工厂都会被使用。

ITransformFactory 提供两种方法,分别是 ValidateBuild。 这些过程一次处理一组转换值,以 IReadOnlyDictionary<string, string> 表示。

加载配置时调用Validate方法以验证内容并报告所有错误。 任何报告的错误都会阻止应用配置。

该方法 Build 采用给定的配置,并为路由生成关联的转换实例。

services.AddReverseProxy()
    .LoadFromConfig(_configuration.GetSection("ReverseProxy"))
    .AddTransformFactory<MyTransformFactory>();
internal class MyTransformFactory : ITransformFactory
{
    public bool Validate(TransformRouteValidationContext context,
        IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("CustomTransform", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomTransform value is required"));
            }

            return true; // Matched
        }

        return false;
    }

    public bool Build(TransformBuilderContext context,
        IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("CustomTransform", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException(
                    "A non-empty CustomTransform value is required");
            }

            context.AddRequestTransform(transformContext =>
            {
                transformContext.ProxyRequest.Options.Set(
                    new HttpRequestOptionsKey<string>("CustomTransform"), value);
                return default;
            });

            return true;
        }

        return false;
    }
}

ValidateBuild 如果已将给定的转换配置标识为它们拥有的配置,则它们会返回 true。 A ITransformFactory 可以实现多个转换。 任何未被任何 RouteConfig.Transforms 处理的 ITransformFactory 条目都会被视为配置错误,并阻止配置生效。

考虑在RouteConfig上添加参数化扩展方法,例如WithTransformQueryValue,以便于程序化路由构建。

public static RouteConfig WithTransformQueryValue(this RouteConfig routeConfig,
    string queryKey, string value, bool append = true)
{
    var type = append ? QueryTransformFactory.AppendKey :
        QueryTransformFactory.SetKey;
    return routeConfig.WithTransform(transform =>
    {
        transform[QueryTransformFactory.QueryValueParameterKey] = queryKey;
        transform[type] = value;
    });
}