YARP Extensibility - Request and Response Transforms (Mở rộng biến đổi Request và Response)
Giới thiệu
Khi proxy một request, thường cần sửa đổi các phần của request hoặc response để thích ứng với yêu cầu của destination server hoặc truyền tải dữ liệu bổ sung như địa chỉ IP gốc của client. Quá trình này được triển khai thông qua Transforms (biến đổi). Các loại transform được định nghĩa toàn cục cho ứng dụng và sau đó các route riêng lẻ cung cấp các tham số để bật và cấu hình các transform đó. Các đối tượng request gốc không bị sửa đổi bởi các transform này, chỉ có các proxy request mới bị ảnh hưởng.
YARP bao gồm một tập hợp các transform request và response tích hợp sẵn có thể được sử dụng. Để biết thêm thông tin, xem YARP Request and Response Transforms. Nếu các transform đó không đủ, thì có thể thêm các transform tùy chỉnh.
RequestTransform
Tất cả request transforms phải kế thừa từ lớp cơ sở trừu tượng RequestTransform. Chúng có thể tự do sửa đổi proxy HttpRequestMessage. Tránh đọc hoặc sửa đổi request body vì điều này có thể làm gián đoạn luồng proxy. Cũng nên cân nhắc thêm một phương thức mở rộng có tham số trên TransformBuilderContext để dễ khám phá và sử dụng.
Một request transform có thể tạo ra một response tức thì theo điều kiện, chẳng hạn cho các điều kiện lỗi. Điều này ngăn các transform còn lại chạy và request được proxy. Điều này được chỉ ra bằng cách đặt HttpResponse.StatusCode thành giá trị khác 200, hoặc gọi HttpResponse.StartAsync(), hoặc ghi vào HttpResponse.Body hoặc BodyWriter.
AddRequestTransform là một phương thức mở rộng TransformBuilderContext định nghĩa một request transform dưới dạng Func<RequestTransformContext, ValueTask>. Điều này cho phép tạo một request transform tùy chỉnh mà không cần triển khai lớp kế thừa RequestTransform.
ResponseTransform
Tất cả response transforms phải kế thừa từ lớp cơ sở trừu tượng ResponseTransform. Chúng có thể tự do sửa đổi client HttpResponse. Tránh đọc hoặc sửa đổi response body vì điều này có thể làm gián đoạn luồng proxy. Cũng nên cân nhắc thêm một phương thức mở rộng có tham số trên TransformBuilderContext để dễ khám phá và sử dụng.
AddResponseTransform là một phương thức mở rộng TransformBuilderContext định nghĩa một response transform dưới dạng Func<ResponseTransformContext, ValueTask>. Điều này cho phép tạo một response transform tùy chỉnh mà không cần triển khai lớp kế thừa ResponseTransform.
ResponseTrailersTransform
Tất cả response trailers transforms phải kế thừa từ lớp cơ sở trừu tượng ResponseTrailersTransform. Chúng có thể tự do sửa đổi các trailers của client HttpResponse. Chúng chạy sau response body và không nên cố gắng sửa đổi response headers hoặc body. Cũng nên cân nhắc thêm một phương thức mở rộng có tham số trên TransformBuilderContext để dễ khám phá và sử dụng.
AddResponseTrailersTransform là một phương thức mở rộng TransformBuilderContext định nghĩa một response trailers transform dưới dạng Func<ResponseTrailersTransformContext, ValueTask>. Điều này cho phép tạo một response trailers transform tùy chỉnh mà không cần triển khai lớp kế thừa ResponseTrailersTransform.
Transforms cho request body
YARP không cung cấp bất kỳ transform tích hợp nào để sửa đổi request body. Tuy nhiên, body có thể được sửa đổi bởi các transform tùy chỉnh.
Hãy thận trọng về các loại request nào được sửa đổi, lượng dữ liệu được buffer, thực thi timeout, phân tích dữ liệu đầu vào không đáng tin cậy, và cập nhật các headers liên quan đến body như Content-Length.
Ví dụ dưới đây sử dụng buffering đơn giản, kém hiệu quả để transform request. Một triển khai hiệu quả hơn sẽ bọc và thay thế HttpContext.Request.Body bằng một stream thực hiện các sửa đổi cần thiết khi dữ liệu được proxy từ client đến server. Điều đó cũng yêu cầu xóa header Content-Length vì độ dài cuối cùng sẽ không được biết trước.
Mẫu này yêu cầu YARP 1.1, xem 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;
}
});
});Các transform tùy chỉnh chỉ có thể sửa đổi request body nếu body đã có sẵn. Chúng không thể thêm body mới vào request không có body (ví dụ: request POST không có body hoặc request GET). Nếu bạn cần thêm body cho một HTTP method và route cụ thể, bạn phải làm điều đó trong middleware chạy trước YARP, không phải trong transform.
Middleware sau đây minh họa cách thêm body vào request không có body:
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;
}
}Bạn có thể sử dụng context.GetRouteModel().Config.RouteId trong middleware để áp dụng logic này có điều kiện cho các YARP route cụ thể.
Transforms cho response body
YARP không cung cấp bất kỳ transform tích hợp nào để sửa đổi response body. Tuy nhiên, body có thể được sửa đổi bởi các transform tùy chỉnh.
Hãy thận trọng về các loại response nào được sửa đổi, lượng dữ liệu được buffer, thực thi timeout, phân tích dữ liệu đầu vào không đáng tin cậy, và cập nhật các headers liên quan đến body như Content-Length. Bạn có thể cần giải nén nội dung trước khi sửa đổi, như được chỉ ra bởi header Content-Encoding, và sau đó nén lại hoặc xóa header.
Ví dụ dưới đây sử dụng buffering đơn giản, kém hiệu quả để transform response. Một triển khai hiệu quả hơn sẽ bọc stream được trả về bởi ReadAsStreamAsync() với một stream thực hiện các sửa đổi cần thiết khi dữ liệu được proxy từ client đến server. Điều đó cũng yêu cầu xóa header Content-Length vì độ dài cuối cùng sẽ không được biết trước.
.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 cung cấp chức năng của AddTransforms được mô tả ở trên cũng như tích hợp DI và hỗ trợ validation (kiểm tra hợp lệ).
ITransformProvider có thể được đăng ký trong DI bằng cách gọi AddTransforms. Nhiều triển khai ITransformProvider có thể được đăng ký và tất cả sẽ được chạy.
ITransformProvider có hai phương thức, Validate và Apply. Validate cho bạn cơ hội kiểm tra route cho bất kỳ tham số nào cần thiết để cấu hình một transform, chẳng hạn như custom metadata (siêu dữ liệu tùy chỉnh), và trả về lỗi validation trên context nếu có bất kỳ giá trị cần thiết nào bị thiếu hoặc không hợp lệ. Phương thức Apply cung cấp chức năng tương tự như AddTransform được thảo luận ở trên, thêm và cấu hình transform theo route.
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
Các nhà phát triển muốn tích hợp các transform tùy chỉnh với section Transforms của cấu hình có thể triển khai ITransformFactory. Điều này nên được đăng ký trong DI bằng phương thức AddTransformFactory<T>(). Nhiều factory có thể được đăng ký và tất cả sẽ được sử dụng.
ITransformFactory cung cấp hai phương thức, Validate và Build. Chúng xử lý một tập hợp các giá trị transform mỗi lần, được đại diện bởi IReadOnlyDictionary<string, string>.
Phương thức Validate được gọi khi tải cấu hình để kiểm tra nội dung và báo cáo tất cả lỗi. Bất kỳ lỗi nào được báo cáo sẽ ngăn cấu hình được áp dụng.
Phương thức Build nhận cấu hình đã cho và tạo ra các instance transform liên kết cho route.
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;
}
}Validate và Build trả về true nếu chúng đã xác định cấu hình transform đã cho là của chúng. Một ITransformFactory có thể triển khai nhiều transform. Bất kỳ mục RouteConfig.Transforms nào không được xử lý bởi bất kỳ ITransformFactory nào sẽ được coi là lỗi cấu hình và ngăn cấu hình được áp dụng.
Cũng nên cân nhắc thêm các phương thức mở rộng có tham số trên RouteConfig như WithTransformQueryValue để tạo điều kiện xây dựng route theo chương trình.
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;
});
}