diff --git a/aspnetcore/fundamentals/servers/yarp/authn-authz.md b/aspnetcore/fundamentals/servers/yarp/authn-authz.md index e384f97adfec..ec43faabde22 100644 --- a/aspnetcore/fundamentals/servers/yarp/authn-authz.md +++ b/aspnetcore/fundamentals/servers/yarp/authn-authz.md @@ -104,10 +104,10 @@ These authentication types are often bound to a specific connection. They are no ### Client Certificates -Client certificates are a TLS feature and are negotiated as part of a connection. See [these docs](/aspnet/core/security/authentication/certauth) for additional information. The certificate can be forwarded to the destination server as an HTTP header using the [ClientCert](transforms.md#clientcert) transform. +Client certificates are a TLS feature and are negotiated as part of a connection. See [these docs](/aspnet/core/security/authentication/certauth) for additional information. The certificate can be forwarded to the destination server as an HTTP header using the [ClientCert](xref:fundamentals/servers/yarp/transforms#clientcert) transform. ### Swapping authentication types Authentication types like Windows that don't flow naturally to the destination server will need to be converted in the proxy to an alternate form. For example a JWT bearer token can be created with the user information and set on the proxy request. -These swaps can be performed using [custom request transforms](transforms.md#from-code). Detailed examples can be developed for specific scenarios if there is enough community interest. We need more community feedback on how you want to convert and flow identity information. +These swaps can be performed using [custom request transforms](xref:fundamentals/servers/yarp/transforms#from-code). Detailed examples can be developed for specific scenarios if there is enough community interest. We need more community feedback on how you want to convert and flow identity information. diff --git a/aspnetcore/fundamentals/servers/yarp/extensibility-transforms.md b/aspnetcore/fundamentals/servers/yarp/extensibility-transforms.md new file mode 100644 index 000000000000..7b4404510865 --- /dev/null +++ b/aspnetcore/fundamentals/servers/yarp/extensibility-transforms.md @@ -0,0 +1,240 @@ +--- +uid: fundamentals/servers/yarp/transform-extensibility +title: YARP Extensibility - Request and Response Transforms +description: YARP Extensibility - Request and Response Transforms +author: samsp-msft +ms.author: samsp +ms.date: 2/14/2025 +ms.topic: article +content_well_notification: AI-contribution +ai-usage: ai-assisted +--- + +# Request and Response Transform Extensibility + +## Introduction +When proxying a request it's common to modify parts of the request or response to adapt to the destination server's requirements or to flow additional data such as the client's original IP address. This process is implemented via Transforms. Types of transforms are defined globally for the application and then individual routes supply the parameters to enable and configure those transforms. The original request objects are not modified by these transforms, only the proxy requests. + +YARP includes a set of built-in request and response transforms that can be used. See [Transforms](./transforms.md) for more details. If those transforms are not sufficient, then custom transfrorms can be added. + +## RequestTransform + +All request transforms must derive from the abstract base class [RequestTransform](xref:fundamentals/servers/yarp/transforms). These can freely modify the proxy `HttpRequestMessage`. Avoid reading or modifying the request body as this may disrupt the proxying flow. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and easy of use. + +A request transform may conditionally produce an immediate response such as for error conditions. This prevents any remaining transforms from running and the request from being proxied. This is indicated by setting the `HttpResponse.StatusCode` to a value other than 200, or calling `HttpResponse.StartAsync()`, or writing to the `HttpResponse.Body` or `BodyWriter`. + +### AddRequestTransform + +[AddRequestTransform](xref:Yarp.ReverseProxy.Transforms.TransformBuilderContextFuncExtensions.AddRequestTransform*) is a `TransformBuilderContext` extension method that defines a request transform as a `Func`. This allows creating a custom request transform without implementing a `RequestTransform` derived class. + +## ResponseTransform + +All response transforms must derive from the abstract base class [ResponseTransform](xref:Yarp.ReverseProxy.Transforms.ResponseTransform). These can freely modify the client `HttpResponse`. Avoid reading or modifying the response body as this may disrupt the proxying flow. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and easy of use. + +### AddResponseTransform + +[AddResponseTransform](xref:Yarp.ReverseProxy.Transforms.TransformBuilderContextFuncExtensions.AddResponseTransform*) is a `TransformBuilderContext` extension method that defines a response transform as a `Func`. This allows creating a custom response transform without implementing a `ResponseTransform` derived class. + +## ResponseTrailersTransform + +All response trailers transforms must derive from the abstract base class [ResponseTrailersTransform](xref:Yarp.ReverseProxy.Transforms.ResponseTrailersTransform). These can freely modify the client HttpResponse trailers. These run after the response body and should not attempt to modify the response headers or body. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and easy of use. + +### AddResponseTrailersTransform + +[AddResponseTrailersTransform](xref:Yarp.ReverseProxy.Transforms.TransformBuilderContextFuncExtensions.AddResponseTrailersTransform*) is a `TransformBuilderContext` extension method that defines a response trailers transform as a `Func`. This allows creating a custom response trailers transform without implementing a `ResponseTrailersTransform` derived class. + +## Request body transforms + +YARP does not provide any built in transforms for modifying the request body. However, the body can be modified in custom transforms. + +Be careful about which kinds of requests are modified, how much data gets buffered, enforcing timeouts, parsing untrusted input, and updating the body-related headers like `Content-Length`. + +The below example uses simple, inefficient buffering to transform requests. A more efficient implementation would wrap and replace `HttpContext.Request.Body` with a stream that performed the needed modifications as data was proxied from client to server. That would also require removing the Content-Length header since the final length would not be known in advance. + +This sample requires YARP 1.1, see https://github.com/microsoft/reverse-proxy/pull/1569. + +```C# +.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; + } + }); +}); +``` + +## Response body transforms + +YARP does not provide any built in transforms for modifying the response body. However, the body can be modified in custom transforms. + +Be careful about which kinds of responses are modified, how much data gets buffered, enforcing timeouts, parsing untrusted input, and updating the body-related headers like `Content-Length`. You may need to decompress content before modifying it, as indicated by the Content-Encoding header, and afterwards re-compress it or remove the header. + +The below example uses simple, inefficient buffering to transform responses. A more efficient implementation would wrap the stream returned by `ReadAsStreamAsync()` with a stream that performed the needed modifications as data was proxied from client to server. That would also require removing the Content-Length header since the final length would not be known in advance. + +```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](xref:Yarp.ReverseProxy.Transforms.Builder.ITransformProvider) provides the functionality of `AddTransforms` described above as well as DI integration and validation support. + +`ITransformProvider`'s can be registered in DI by calling [AddTransforms<T>()](xref:Microsoft.Extensions.DependencyInjection.ReverseProxyServiceCollectionExtensions). Multiple `ITransformProvider` implementations can be registered and all will be run. + +`ITransformProvider` has two methods, `Validate` and `Apply`. `Validate` gives you the opportunity to inspect the route for any parameters that are needed to configure a transform, such as custom metadata, and to return validation errors on the context if any needed values are missing or invalid. The `Apply` method provides the same functionality as AddTransform as discussed above, adding and configuring transforms per route. + +```C# +services.AddReverseProxy() + .LoadFromConfig(_configuration.GetSection("ReverseProxy")) + .AddTransforms(); +``` +```C# +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("CustomMetadata"), value); + return default; + }); + } + } +} +``` + +## ITransformFactory + +Developers that want to integrate their custom transforms with the `Transforms` section of configuration can implement an [ITransformFactory](xref:Yarp.ReverseProxy.Transforms.Builder.ITransformFactory). This should be registered in DI using the `AddTransformFactory()` method. Multiple factories can be registered and all will be used. + +`ITransformFactory` provides two methods, `Validate` and `Build`. These process one set of transform values at a time, represented by a `IReadOnlyDictionary`. + +The `Validate` method is called when loading a configuration to verify the contents and report all errors. Any reported errors will prevent the configuration from being applied. + +The `Build` method takes the given configuration and produces the associated transform instances for the route. + +```C# +services.AddReverseProxy() + .LoadFromConfig(_configuration.GetSection("ReverseProxy")) + .AddTransformFactory(); +``` +```C# +internal class MyTransformFactory : ITransformFactory +{ + public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary 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 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("CustomTransform"), value); + return default; + }); + + return true; + } + + return false; + } +} +``` + +`Validate` and `Build` return `true` if they've identified the given transform configuration as one that they own. A `ITransformFactory` may implement multiple transforms. Any `RouteConfig.Transforms` entries not handled by any `ITransformFactory` will be considered configuration errors and prevent the configuration from being applied. + +Consider also adding parametrized extension methods on `RouteConfig` like `WithTransformQueryValue` to facilitate programmatic route construction. + +```C# +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; + }); +} +``` \ No newline at end of file diff --git a/aspnetcore/fundamentals/servers/yarp/header-guidelines.md b/aspnetcore/fundamentals/servers/yarp/header-guidelines.md index f99897c2970a..6e086033fa20 100644 --- a/aspnetcore/fundamentals/servers/yarp/header-guidelines.md +++ b/aspnetcore/fundamentals/servers/yarp/header-guidelines.md @@ -60,15 +60,15 @@ This header instructs clients to always use HTTPS, but there may be a conflict b ### Host -The Host header indicates which site on the server the request is intended for. This header is removed by default since the host name used publicly by the proxy is likely to differ from the one used by the service behind the proxy. This is configurable using the [RequestHeaderOriginalHost](transforms.md#requestheaderoriginalhost) transform. +The Host header indicates which site on the server the request is intended for. This header is removed by default since the host name used publicly by the proxy is likely to differ from the one used by the service behind the proxy. This is configurable using the [RequestHeaderOriginalHost](xref:fundamentals/servers/yarp/transforms#requestheaderoriginalhost) transform. ### X-Forwarded-*, Forwarded -Because a separate connection is used to communicate with the destination, these request headers can be used to forward information about the original connection like the IP, scheme, port, client certificate, etc.. X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-Prefix are enabled by default. This information is subject to spoofing attacks so any existing headers on the request are removed and replaced by default. The destination app should be careful about how much trust it places in these values. See [transforms](transforms.md#defaults) for configuring these in the proxy. See the [ASP.NET docs](/aspnet/core/host-and-deploy/proxy-load-balancer) for configuring the destination app to read these headers. +Because a separate connection is used to communicate with the destination, these request headers can be used to forward information about the original connection like the IP, scheme, port, client certificate, etc.. X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-Prefix are enabled by default. This information is subject to spoofing attacks so any existing headers on the request are removed and replaced by default. The destination app should be careful about how much trust it places in these values. See [transforms](xref:fundamentals/servers/yarp/transforms#defaults) for configuring these in the proxy. See the [ASP.NET docs](/aspnet/core/host-and-deploy/proxy-load-balancer) for configuring the destination app to read these headers. ### X-http-method-override, x-http-method, x-method-override -Some clients and servers limit which HTTP methods they allow (GET, POST, etc.). These request headers are sometimes used to work around those restrictions. These headers are proxied by default. If in the proxy you want to prevent these bypasses then use the [RequestHeaderRemove](transforms.md#requestheaderremove) transform. +Some clients and servers limit which HTTP methods they allow (GET, POST, etc.). These request headers are sometimes used to work around those restrictions. These headers are proxied by default. If in the proxy you want to prevent these bypasses then use the [RequestHeaderRemove](xref:fundamentals/servers/yarp/transforms#requestheaderremove) transform. ### Set-Cookie @@ -80,8 +80,8 @@ This response header is used with redirects and may contain a scheme, domain, an ### Server -This response header indicates what server technology was used to generate the response (IIS, Kestrel, etc.). This header is proxied from the destination by default. Applications that want to remove it can use the [ResponseHeaderRemove](transforms.md#responseheaderremove) transform, in which case the proxy's default server header will be used. Suppressing the proxy default server header is server specific, such as for [Kestrel](/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.addserverheader#Microsoft_AspNetCore_Server_Kestrel_Core_KestrelServerOptions_AddServerHeader). +This response header indicates what server technology was used to generate the response (IIS, Kestrel, etc.). This header is proxied from the destination by default. Applications that want to remove it can use the [ResponseHeaderRemove](xref:fundamentals/servers/yarp/transforms#responseheaderremove) transform, in which case the proxy's default server header will be used. Suppressing the proxy default server header is server specific, such as for [Kestrel](/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.addserverheader#Microsoft_AspNetCore_Server_Kestrel_Core_KestrelServerOptions_AddServerHeader). ### X-Powered-By -This response header indicates what web framework was used to generate the response (ASP.NET, etc.). ASP.NET Core does not generate this header but IIS can. This header is proxied from the destination by default. Applications that want to remove it can use the [ResponseHeaderRemove](transforms.md#responseheaderremove) transform. +This response header indicates what web framework was used to generate the response (ASP.NET, etc.). ASP.NET Core does not generate this header but IIS can. This header is proxied from the destination by default. Applications that want to remove it can use the [ResponseHeaderRemove](xref:fundamentals/servers/yarp/transforms#responseheaderremove) transform. diff --git a/aspnetcore/fundamentals/servers/yarp/https-tls.md b/aspnetcore/fundamentals/servers/yarp/https-tls.md index 07cb0afec9de..1aad6f4bf7fd 100644 --- a/aspnetcore/fundamentals/servers/yarp/https-tls.md +++ b/aspnetcore/fundamentals/servers/yarp/https-tls.md @@ -34,7 +34,7 @@ Kestrel supports intercepting incoming connections before the TLS handshake. YAR To enable TLS encryption when communicating with a destination specify the destination address as `https` like `"https://destinationHost"`. See the [configuration docs](config-files.md#configuration-structure) for examples. -The host name specified in the destination address will be used for the TLS handshake by default, including SNI and server certificate validation. If proxying the [original host header](transforms.md#requestheaderoriginalhost) is enabled, that value will be used for the TLS handshake instead. If a custom host value needs to be used then use the [RequestHeader](transforms.md#requestheader) transform to set the host header. +The host name specified in the destination address will be used for the TLS handshake by default, including SNI and server certificate validation. If proxying the [original host header](xref:fundamentals/servers/yarp/transforms#requestheaderoriginalhost) is enabled, that value will be used for the TLS handshake instead. If a custom host value needs to be used then use the [RequestHeader](xref:fundamentals/servers/yarp/transforms#requestheader) transform to set the host header. Outbound connections to the destinations are handled by HttpClient/SocketsHttpHandler. A different instance and settings can be configured per cluster. Some settings are available in the configuration model, while others can only be configured in code. See the [HttpClient](xref:fundamentals/servers/yarp/http-client-config) docs for details. diff --git a/aspnetcore/fundamentals/servers/yarp/transforms-request.md b/aspnetcore/fundamentals/servers/yarp/transforms-request.md new file mode 100644 index 000000000000..f9b2936d322a --- /dev/null +++ b/aspnetcore/fundamentals/servers/yarp/transforms-request.md @@ -0,0 +1,601 @@ +--- +uid: fundamentals/servers/yarp/transforms-request +title: YARP Request Transforms +description: YARP Request Transforms +author: samsp-msft +ms.author: samsp +ms.date: 2/6/2025 +ms.topic: article +content_well_notification: AI-contribution +ai-usage: ai-assisted +--- + +# Request transforms + +Request transforms include the request path, query, HTTP version, method, and headers. In code these are represented by the [RequestTransformContext](xref:Yarp.ReverseProxy.Transforms.RequestTransformContext) object and processed by implementations of the abstract class [RequestTransform](xref:Yarp.ReverseProxy.Transforms.RequestTransform). + +Notes: +- The proxy request scheme (http/https), authority, and path base, are taken from the destination server address (`https://localhost:10001/Path/Base` in the example above) and should not be modified by transforms. +- The Host header can be overridden by transforms independent of the authority, see [RequestHeader](#requestheader) below. +- The request's original PathBase property is not used when constructing the proxy request, see [X-Forwarded](#x-forwarded). +- All incoming request headers are copied to the proxy request by default with the exception of the Host header (see `Defaults` [Defaults](xref:fundamentals/servers/yarp/timeouts#defaults)). [X-Forwarded](#x-forwarded) headers are also added by default. These behaviors can be configured using the following transforms. Additional request headers can be specified, or request headers can be excluded by setting them to an empty value. + +The following are built in transforms identified by their primary config key. These transforms are applied in the order they are specified in the route configuration. + +## PathPrefix + +**Modifies the request path adding a prefix value** + +| Key | Value | Required | +|-----|-------|----------| +| PathPrefix | A path starting with a '/' | yes | + +Config: +```JSON +{ "PathPrefix": "/prefix" } +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformPathPrefix(prefix: "/prefix"); +``` +```C# +transformBuilderContext.AddPathPrefix(prefix: "/prefix"); +``` +Example:
+`/request/path` becomes `/prefix/request/path` + +This will prefix the request path with the given value. + +## PathRemovePrefix + +**Modifies the request path removing a prefix value** + +| Key | Value | Required | +|-----|-------|----------| +| PathRemovePrefix | A path starting with a '/' | yes | + +Config: +```JSON +{ "PathRemovePrefix": "/prefix" } +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformPathRemovePrefix(prefix: "/prefix"); +``` +```csharp +transformBuilderContext.AddPathRemovePrefix(prefix: "/prefix"); +``` +Example:
+`/prefix/request/path` becomes `/request/path`
+`/prefix2/request/path` is not modified
+ +This will remove the matching prefix from the request path. Matches are made on path segment boundaries (`/`). If the prefix does not match then no changes are made. + +## PathSet + +**Replaces the request path with the specified value** + +| Key | Value | Required | +|-----|-------|----------| +| PathSet | A path starting with a '/' | yes | + +Config: +```JSON +{ "PathSet": "/newpath" } +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformPathSet(path: "/newpath"); +``` +```C# +transformBuilderContext.AddPathSet(path: "/newpath"); +``` +Example:
+`/request/path` becomes `/newpath` + +This will set the request path with the given value. + +## PathPattern + +**Replaces the request path using a pattern template** + +| Key | Value | Required | +|-----|-------|----------| +| PathPattern | A path template starting with a '/' | yes | + +Config: +```JSON +{ "PathPattern": "/my/{plugin}/api/{**remainder}" } +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformPathRouteValues(pattern: new PathString("/my/{plugin}/api/{**remainder}")); +``` +```C# +transformBuilderContext.AddPathRouteValues(pattern: new PathString("/my/{plugin}/api/{**remainder}")); +``` + +This will set the request path with the given value and replace any `{}` segments with the associated route value. `{}` segments without a matching route value are removed. The final `{}` segment can be marked as `{**remainder}` to indicate this is a catch-all segment that may contain multiple path segments. See ASP.NET Core's [routing docs](/aspnet/core/fundamentals/routing#route-template-reference) for more information about route templates. + +Example: + +| Step | Value | +|------|-------| +| Route definition | `/api/{plugin}/stuff/{**remainder}` | +| Request path | `/api/v1/stuff/more/stuff` | +| Plugin value | `v1` | +| Remainder value | `more/stuff` | +| PathPattern | `/my/{plugin}/api/{**remainder}` | +| Result | `/my/v1/api/more/stuff` | + +## QueryValueParameter + +**Adds or replaces parameters in the request query string** + +| Key | Value | Required | +|-----|-------|----------| +| QueryValueParameter | Name of a query string parameter | yes | +| Set/Append | Static value | yes | + +Config: +```JSON +{ + "QueryValueParameter": "foo", + "Append": "bar" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformQueryValue(queryKey: "foo", value: "bar", append: true); +``` +```C# +transformBuilderContext.AddQueryValue(queryKey: "foo", value: "bar", append: true); +``` + +This will add a query string parameter with the name `foo` and sets it to the static value `bar`. + +Example: + +| Step | Value | +|------|-------| +| Query | `?a=b` | +| QueryValueParameter | `foo` | +| Append | `remainder` | +| Result | `?a=b&foo=remainder` | + +## QueryRouteParameter + +**Adds or replaces a query string parameter with a value from the route configuration** + +| Key | Value | Required | +|-----|-------|----------| +| QueryRouteParameter | Name of a query string parameter | yes | +| Set/Append | The name of a route value | yes | + +Config: +```JSON +{ + "QueryRouteParameter": "foo", + "Append": "remainder" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformQueryRouteValue(queryKey: "foo", routeValueKey: "remainder", append: true); +``` +```C# +transformBuilderContext.AddQueryRouteValue(queryKey: "foo", routeValueKey: "remainder", append: true); +``` + +This will add a query string parameter with the name `foo` and sets it to the value of the associated route value. + +Example: + +| Step | Value | +|------|-------| +| Route definition | `/api/{*remainder}` | +| Request path | `/api/more/stuff` | +| Remainder value | `more/stuff` | +| QueryRouteParameter | `foo` | +| Append | `remainder` | +| Result | `?foo=more/stuff` | + +## QueryRemoveParameter + +**Removes the specified parameter from the request query string** + +| Key | Value | Required | +|-----|-------|----------| +| QueryRemoveParameter | Name of a query string parameter | yes | + +Config: +```JSON +{ "QueryRemoveParameter": "foo" } +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformQueryRemoveKey(queryKey: "foo"); +``` +```C# +transformBuilderContext.AddQueryRemoveKey(queryKey: "foo"); +``` + +This will remove a query string parameter with the name `foo` if present on the request. + +Example: + +| Step | Value | +|------|-------| +| Request path | `?a=b&foo=c` | +| QueryRemoveParameter | `foo` | +| Result | `?a=b` | + +## HttpMethodChange + +**Changes the http method used in the request** + +| Key | Value | Required | +|-----|-------|----------| +| HttpMethodChange | The http method to replace | yes | +| Set | The new http method | yes | + +Config: +```JSON +{ + "HttpMethodChange": "PUT", + "Set": "POST" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformHttpMethodChange(fromHttpMethod: HttpMethods.Put, toHttpMethod: HttpMethods.Post); +``` +```C# +transformBuilderContext.AddHttpMethodChange(fromHttpMethod: HttpMethods.Put, toHttpMethod: HttpMethods.Post); +``` + +This will change PUT requests to POST. + +## RequestHeadersCopy + +**Sets whether incoming request headers are copied to the outbound request** + +| Key | Value | Default | Required | +|-----|-------|---------|----------| +| RequestHeadersCopy | true/false | true | yes | + +Config: +```JSON +{ "RequestHeadersCopy": "false" } +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformCopyRequestHeaders(copy: false); +``` +```C# +transformBuilderContext.CopyRequestHeaders = false; +``` + +This sets if all incoming request headers are copied to the proxy request. This setting is enabled by default and can by disabled by configuring the transform with a `false` value. Transforms that reference specific headers will still be run if this is disabled. + +## RequestHeaderOriginalHost + +**Specifies if the incoming request Host header should be copied to the proxy request** + +| Key | Value | Default | Required | +|-----|-------|---------|----------| +| RequestHeaderOriginalHost | true/false | false | yes | + +Config: +```JSON +{ "RequestHeaderOriginalHost": "true" } +``` +```csharp +routeConfig = routeConfig.WithTransformUseOriginalHostHeader(useOriginal: true); +``` +```C# +transformBuilderContext.AddOriginalHost(true); +``` + +This specifies if the incoming request Host header should be copied to the proxy request. This setting is disabled by default and can be enabled by configuring the transform with a `true` value. Transforms that directly reference the `Host` header will override this transform. + +## RequestHeader + +**Adds or replaces request headers** + +| Key | Value | Required | +|-----|-------|----------| +| RequestHeader | The header name | yes | +| Set/Append | The header value | yes | + +Config: +```JSON +{ + "RequestHeader": "MyHeader", + "Set": "MyValue" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformRequestHeader(headerName: "MyHeader", value: "MyValue", append: false); +``` +```C# +transformBuilderContext.AddRequestHeader(headerName: "MyHeader", value: "MyValue", append: false); +``` + +Example: +``` +MyHeader: MyValue +``` + +This sets or appends the value for the named header. Set replaces any existing header. Append adds an additional header with the given value. +Note: setting "" as a header value is not recommended and can cause an undefined behavior. + +## RequestHeaderRouteValue + +**Adds or replaces a header with a value from the route configuration** + +| Key | Value | Required | +|-----|-------|----------| +| RequestHeader | Name of a query string parameter | yes | +| Set/Append | The name of a route value | yes | + +Config: +```JSON +{ + "RequestHeaderRouteValue": "MyHeader", + "Set": "MyRouteKey" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformRequestHeaderRouteValue(headerName: "MyHeader", routeValueKey: "key", append: false); +``` +```C# +transformBuilderContext.AddRequestHeaderRouteValue(headerName: "MyHeader", routeValueKey: "key", append: false); +``` + +Example: + +| Step | Value | +|------|---------------------| +| Route definition | `/api/{*remainder}` | +| Request path | `/api/more/stuff` | +| Remainder value | `more/stuff` | +| RequestHeaderFromRoute | `foo` | +| Append | `remainder` | +| Result | `foo: more/stuff` | + +This sets or appends the value for the named header with a value from the route configuration. Set replaces any existing header. Append adds an additional header with the given value. +Note: setting "" as a header value is not recommended and can cause an undefined behavior. + +## RequestHeaderRemove + +**Removes request headers** + +| Key | Value | Required | +|-----|-------|----------| +| RequestHeaderRemove | The header name | yes | + +Config: +```JSON +{ + "RequestHeaderRemove": "MyHeader" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformRequestHeaderRemove(headerName: "MyHeader"); +``` +```C# +transformBuilderContext.AddRequestHeaderRemove(headerName: "MyHeader"); +``` + +Example: +``` +MyHeader: MyValue +AnotherHeader: AnotherValue +``` + +This removes the named header. + +## RequestHeadersAllowed + +| Key | Value | Required | +|-----|-------|----------| +| RequestHeadersAllowed | A semicolon separated list of allowed header names. | yes | + +Config: +```JSON +{ + "RequestHeadersAllowed": "Header1;header2" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformRequestHeadersAllowed("Header1", "header2"); +``` +```C# +transformBuilderContext.AddRequestHeadersAllowed("Header1", "header2"); +``` + +YARP copies most request headers to the proxy request by default (see [RequestHeadersCopy](xref:fundamentals/servers/yarp/transforms#requestheaderscopy)). Some security models only allow specific headers to be proxied. This transform disables RequestHeadersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list. + +Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities. + +Example: +``` +Header1: value1 +Header2: value2 +AnotherHeader: AnotherValue +``` + +Only header1 and header2 are copied to the proxy request. + +## X-Forwarded + +**Adds headers with information about the original client request** + +| Key | Value | Default | Required | +|-----|-------|---------|----------| +| X-Forwarded | Default action (Set, Append, Remove, Off) to apply to all X-Forwarded-* listed below | Set | yes | +| For | Action to apply to this header | * See X-Forwarded | no | +| Proto | Action to apply to this header | * See X-Forwarded | no | +| Host | Action to apply to this header | * See X-Forwarded | no | +| Prefix | Action to apply to this header | * See X-Forwarded | no | +| HeaderPrefix | The header name prefix | "X-Forwarded-" | no | + +Action "Off" completely disables the transform. + +Config: +```JSON +{ + "X-Forwarded": "Set", + "For": "Remove", + "Proto": "Append", + "Prefix": "Off", + "HeaderPrefix": "X-Forwarded-" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformXForwarded( + headerPrefix = "X-Forwarded-", + ForwardedTransformActions xDefault = ForwardedTransformActions.Set, + ForwardedTransformActions? xFor = null, + ForwardedTransformActions? xHost = null, + ForwardedTransformActions? xProto = null, + ForwardedTransformActions? xPrefix = null); +``` +```C# +transformBuilderContext.AddXForwarded(ForwardedTransformActions.Set); +transformBuilderContext.AddXForwardedFor(headerName: "X-Forwarded-For", ForwardedTransformActions.Append); +transformBuilderContext.AddXForwardedHost(headerName: "X-Forwarded-Host", ForwardedTransformActions.Append); +transformBuilderContext.AddXForwardedProto(headerName: "X-Forwarded-Proto", ForwardedTransformActions.Off); +transformBuilderContext.AddXForwardedPrefix(headerName: "X-Forwarded-Prefix", ForwardedTransformActions.Remove); +``` + +Example: +``` +X-Forwarded-For: 5.5.5.5 +X-Forwarded-Proto: https +X-Forwarded-Host: IncomingHost:5000 +X-Forwarded-Prefix: /path/base +``` +Disable default headers: +```JSON +{ "X-Forwarded": "Off" } +``` +```C# +transformBuilderContext.UseDefaultForwarders = false; +``` + +When the proxy connects to the destination server, the connection is indepenent from the one the client made to the proxy. The destination server likely needs original connection information for security checks and to properly generate absolute URIs for links and redirects. To enable information about the client connection to be passed to the destination a set of extra headers can be added. Until the `Forwarded` standard was created, a common solution is to use `X-Forwarded-*` headers. There is no official standard that defines the `X-Forwarded-*` headers and implementations vary, check your destination server for support. + +This transform is enabled by default even if not specified in the route config. + +Set the `X-Forwarded` value to a comma separated list containing the headers you need to enable. All for headers are enabled by default. All can be disabled by specifying the value `"Off"`. + + +The Prefix specifies the header name prefix to use for each header. With the default `X-Forwarded-` prefix the resulting headers will be `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-Prefix`. + +Transform action specifies how each header should be combined with an existing header of the same name. It can be "Set", "Append", "Remove, or "Off" (completely disable the transform). A request traversing multiple proxies may accumulate a list of such headers and the destination server will need to evaluate the list to determine the original value. If action is "Set" and the associated value is not available on the request (e.g. RemoteIpAddress is null), any existing header is still removed to prevent spoofing. + +The {Prefix}For header value is taken from `HttpContext.Connection.RemoteIpAddress` representing the prior caller's IP address. The port is not included. IPv6 addresses do not include the bounding `[]` brackets. + +The {Prefix}Proto header value is taken from `HttpContext.Request.Scheme` indicating if the prior caller used HTTP or HTTPS. + +The {Prefix}Host header value is taken from the incoming request's Host header. This is independent of RequestHeaderOriginalHost specified above. Unicode/IDN hosts are punycode encoded. + +The {Prefix}Prefix header value is taken from `HttpContext.Request.PathBase`. The PathBase property is not used when generating the proxy request so the destination server will need the original value to correctly generate links and directs. The value is in the percent encoded Uri format. + +## Forwarded + +**Adds a header with information about the original client request** + +| Key | Value | Default | Required | +|-----|-------|---------|----------| +| Forwarded | A comma separated list containing any of these values: for,by,proto,host | (none) | yes | +| ForFormat | Random/RandomAndPort/RandomAndRandomPort/Unknown/UnknownAndPort/UnknownAndRandomPort/Ip/IpAndPort/IpAndRandomPort | Random | no | +| ByFormat | Random/RandomAndPort/RandomAndRandomPort/Unknown/UnknownAndPort/UnknownAndRandomPort/Ip/IpAndPort/IpAndRandomPort | Random | no | +| Action | Action to apply to this header (Set, Append, Remove, Off) | Set | no | + +Config: +```JSON +{ + "Forwarded": "by,for,host,proto", + "ByFormat": "Random", + "ForFormat": "IpAndPort", + "Action": "Append" +}, +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformForwarded(useHost: true, useProto: true, forFormat: NodeFormat.IpAndPort, ByFormat: NodeFormat.Random, action: ForwardedTransformAction.Append); +``` +```C# +transformBuilderContext.AddForwarded(useHost: true, useProto: true, forFormat: NodeFormat.IpAndPort, ByFormat: NodeFormat.Random, action: ForwardedTransformAction.Append); +``` +Example: +``` +Forwarded: proto=https;host="localhost:5001";for="[::1]:20173";by=_YQuN68tm6 +``` + +The `Forwarded` header is defined by [RFC 7239](https://tools.ietf.org/html/rfc7239). It consolidates many of the same functions as the unofficial X-Forwarded headers, flowing information to the destination server that would otherwise be obscured by using a proxy. + +Enabling this transform will disable the default X-Forwarded transforms as they carry similar information in another format. The X-Forwarded transforms can still be explicitly enabled. + +Action: This specifies how the transform should handle an existing Forwarded header. It can be "Set", "Append", "Remove, or "Off" (completely disable the transform). A request traversing multiple proxies may accumulate a list of such headers and the destination server will need to evaluate the list to determine the original value. + +Proto: This value is taken from `HttpContext.Request.Scheme` indicating if the prior caller used HTTP or HTTPS. + +Host: This value is taken from the incoming request's Host header. This is independent of RequestHeaderOriginalHost specified above. Unicode/IDN hosts are punycode encoded. + +For: This value identifies the prior caller. IP addresses are taken from `HttpContext.Connection.RemoteIpAddress`. See ByFormat and ForFormat below for details. + +By: This value identifies where the proxy received the request. IP addresses are taken from `HttpContext.Connection.LocalIpAddress`. See ByFormat and ForFormat below for details. + +ByFormat and ForFormat: + +The RFC allows a [variety of formats](https://tools.ietf.org/html/rfc7239#section-6) for the By and For fields. It requires that the default format uses an obfuscated identifier identified here as Random. + +| Format | Description | Example | +|--------|-------------|---------| +| Random | An obfuscated identifier that is generated randomly per request. This allows for diagnostic tracing scenarios while limiting the flow of uniquely identifying information for privacy reasons. | `by=_YQuN68tm6` | +| RandomAndPort | The Random identifier plus the port. | `by="_YQuN68tm6:80"` | +| RandomAndRandomPort | The Random identifier plus another random identifier for the port. | `by="_YQuN68tm6:_jDw5Cf3tQ"` | +| Unknown | This can be used when the identity of the preceding entity is not known, but the proxy server still wants to signal that the request was forwarded. | `by=unknown` | +| UnknownAndPort | The Unknown identifier plus the port if available. | `by="unknown:80"` | +| UnknownAndRandomPort | The Unknown identifier plus random identifier for the port. | `by="unknown:_jDw5Cf3tQ"` | +| Ip | An IPv4 address or an IPv6 address including brackets. | `by="[::1]"` | +| IpAndPort | The IP address plus the port. | `by="[::1]:80"` | +| IpAndRandomPort | The IP address plus random identifier for the port. | `by="[::1]:_jDw5Cf3tQ"` | + +## ClientCert + +**Forwards the client cert used on the inbound connection as a header to destination** + +| Key | Value | Required | +|-----|-------|----------| +| ClientCert | The header name | yes | + +Config: +```JSON +{ "ClientCert": "X-Client-Cert" } +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformClientCertHeader(headerName: "X-Client-Cert"); +``` +```C# +transformBuilderContext.AddClientCertHeader(headerName: "X-Client-Cert"); +``` +Example: +``` +X-Client-Cert: SSdtIGEgY2VydGlmaWNhdGU... +``` +As the inbound and outbound connections are independent, there needs to be a way to pass any inbound client certificate to the destination server. This transform causes the client certificate taken from `HttpContext.Connection.ClientCertificate` to be Base64 encoded and set as the value for the given header name. The destination server may need that certificate to authenticate the client. There is no standard that defines this header and implementations vary, check your destination server for support. + +Servers do minimal validation on the incoming client certificate by default. The certificate should be validated either in the proxy or the destination, see the [client certificate auth](/aspnet/core/security/authentication/certauth) docs for details. + +This transform will only apply if the client certificate is already present on the connection. See the [optional certs doc](/aspnet/core/security/authentication/certauth#optional-client-certificates) if it needs to be requested from the client on a per-route basis. \ No newline at end of file diff --git a/aspnetcore/fundamentals/servers/yarp/transforms-response.md b/aspnetcore/fundamentals/servers/yarp/transforms-response.md new file mode 100644 index 000000000000..0dfa379a43de --- /dev/null +++ b/aspnetcore/fundamentals/servers/yarp/transforms-response.md @@ -0,0 +1,262 @@ +--- +uid: fundamentals/servers/yarp/transforms-response +title: YARP Response and Response Trailer Transforms +description: YARP Response and Response Trailer Transforms +author: samsp-msft +ms.author: samsp +ms.date: 2/6/2025 +ms.topic: article +content_well_notification: AI-contribution +ai-usage: ai-assisted +--- + +# Response and Response Trailers + +All response headers and trailers are copied from the proxied response to the outgoing client response by default. Response and response trailer transforms may specify if they should be applied only for successful responses or for all responses. + +In code these are implemented as derivations of the abstract classes [ResponseTransform](xref:Yarp.ReverseProxy.Transforms.ResponseTransform) and [ResponseTrailersTransform](xref:Yarp.ReverseProxy.Transforms.ResponseTrailersTransform). + +## ResponseHeadersCopy + +**Sets whether destination response headers are copied to the client** + +| Key | Value | Default | Required | +|-----|-------|---------|----------| +| ResponseHeadersCopy | true/false | true | yes | + +Config: +```JSON +{ "ResponseHeadersCopy": "false" } +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformCopyResponseHeaders(copy: false); +``` +```C# +transformBuilderContext.CopyResponseHeaders = false; +``` + +This sets if all proxy response headers are copied to the client response. This setting is enabled by default and can be disabled by configuring the transform with a `false` value. Transforms that reference specific headers will still be run if this is disabled. + +## ResponseHeader + +**Adds or replaces response headers** + +| Key | Value | Default | Required | +|-----|-------|---------|----------| +| ResponseHeader | The header name | (none) | yes | +| Set/Append | The header value | (none) | yes | +| When | Success/Always/Failure | Success | no | + +Config: +```JSON +{ + "ResponseHeader": "HeaderName", + "Append": "value", + "When": "Success" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformResponseHeader(headerName: "HeaderName", value: "value", append: true, ResponseCondition.Success); +``` +```C# +transformBuilderContext.AddResponseHeader(headerName: "HeaderName", value: "value", append: true, always: ResponseCondition.Success); +``` +Example: +``` +HeaderName: value +``` + +This sets or appends the value for the named response header. Set replaces any existing header. Append adds an additional header with the given value. +Note: setting "" as a header value is not recommended and can cause an undefined behavior. + +`When` specifies if the response header should be included for all, successful, or failure responses. Any response with a status code less than 400 is considered a success. + +## ResponseHeaderRemove + +**Removes response headers** + +| Key | Value | Default | Required | +|-----|-------|---------|----------| +| ResponseHeaderRemove | The header name | (none) | yes | +| When | Success/Always/Failure | Success | no | + +Config: +```JSON +{ + "ResponseHeaderRemove": "HeaderName", + "When": "Success" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformResponseHeaderRemove(headerName: "HeaderName", ResponseCondition.Success); +``` +```C# +transformBuilderContext.AddResponseHeaderRemove(headerName: "HeaderName", ResponseCondition.Success); +``` +Example: +``` +HeaderName: value +AnotherHeader: another-value +``` + +This removes the named response header. + +`When` specifies if the response header should be removed for all, successful, or failure responses. Any response with a status code less than 400 is considered a success. + +## ResponseHeadersAllowed + +| Key | Value | Required | +|-----|-------|----------| +| ResponseHeadersAllowed | A semicolon separated list of allowed header names. | yes | + +Config: +```JSON +{ + "ResponseHeadersAllowed": "Header1;header2" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformResponseHeadersAllowed("Header1", "header2"); +``` +```C# +transformBuilderContext.AddResponseHeadersAllowed("Header1", "header2"); +``` + +YARP copies most response headers from the proxy response by default (see [ResponseHeadersCopy](xref:fundamentals/servers/yarp/transforms#responseheaderscopy)). Some security models only allow specific headers to be proxied. This transform disables ResponseHeadersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list. + +Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities. + +Example: +``` +Header1: value1 +Header2: value2 +AnotherHeader: AnotherValue +``` + +Only header1 and header2 are copied from the proxy response. + +## ResponseTrailersCopy + +**Sets whether destination trailing response headers are copied to the client** + +| Key | Value | Default | Required | +|-----|-------|---------|----------| +| ResponseTrailersCopy | true/false | true | yes | + +Config: +```JSON +{ "ResponseTrailersCopy": "false" } +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformCopyResponseTrailers(copy: false); +``` +```C# +transformBuilderContext.CopyResponseTrailers = false; +``` + +This sets if all proxy response trailers are copied to the client response. This setting is enabled by default and can be disabled by configuring the transform with a `false` value. Transforms that reference specific headers will still be run if this is disabled. + +## ResponseTrailer + +**Adds or replaces trailing response headers** + +| Key | Value | Default | Required | +|-----|-------|---------|----------| +| ResponseTrailer | The header name | (none) | yes | +| Set/Append | The header value | (none) | yes | +| When | Success/Always/Failure | Success | no | + +Config: +```JSON +{ + "ResponseTrailer": "HeaderName", + "Append": "value", + "When": "Success" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformResponseTrailer(headerName: "HeaderName", value: "value", append: true, ResponseCondition.Success); +``` +```C# +transformBuilderContext.AddResponseTrailer(headerName: "HeaderName", value: "value", append: true, ResponseCondition.Success); +``` +Example: +``` +HeaderName: value +``` + +Response trailers are headers sent at the end of the response body. Support for trailers is uncommon in HTTP/1.1 implementations but is becoming common in HTTP/2 implementations. Check your client and server for support. + +ResponseTrailer follows the same structure and guidance as [ResponseHeader](xref:fundamentals/servers/yarp/transforms#responseheader). + +## ResponseTrailerRemove + +**Removes trailing response headers** + +| Key | Value | Default | Required | +|-----|-------|---------|----------| +| ResponseTrailerRemove | The header name | (none) | yes | +| When | Success/Always/Failure | Success | no | + +Config: +```JSON +{ + "ResponseTrailerRemove": "HeaderName", + "When": "Success" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformResponseTrailerRemove(headerName: "HeaderName", ResponseCondition.Success); +``` +```C# +transformBuilderContext.AddResponseTrailerRemove(headerName: "HeaderName", ResponseCondition.Success); +``` +Example: +``` +HeaderName: value +AnotherHeader: another-value +``` + +This removes the named trailing header. + +ResponseTrailerRemove follows the same structure and guidance as [ResponseHeaderRemove](xref:fundamentals/servers/yarp/transforms#responseheaderremove). + +## ResponseTrailersAllowed + +| Key | Value | Required | +|-----|-------|----------| +| ResponseTrailersAllowed | A semicolon separated list of allowed header names. | yes | + +Config: +```JSON +{ + "ResponseTrailersAllowed": "Header1;header2" +} +``` +Code: +```csharp +routeConfig = routeConfig.WithTransformResponseTrailersAllowed("Header1", "header2"); +``` +```C# +transformBuilderContext.AddResponseTrailersAllowed("Header1", "header2"); +``` + +YARP copies most response trailers from the proxy response by default (see [ResponseTrailersCopy](xref:fundamentals/servers/yarp/transforms#responsetrailerscopy)). Some security models only allow specific headers to be proxied. This transform disables ResponseTrailersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list. + +Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities. + +Example: +``` +Header1: value1 +Header2: value2 +AnotherHeader: AnotherValue +``` + +Only header1 and header2 are copied from the proxy response. \ No newline at end of file diff --git a/aspnetcore/fundamentals/servers/yarp/transforms.md b/aspnetcore/fundamentals/servers/yarp/transforms.md index 4799782a103d..7f43723694b2 100644 --- a/aspnetcore/fundamentals/servers/yarp/transforms.md +++ b/aspnetcore/fundamentals/servers/yarp/transforms.md @@ -19,11 +19,11 @@ Request and response body transforms are not provided by YARP but you can write ## Defaults The following transforms are enabled by default for all routes. They can be configured or disabled as shown later in this document. -- Host - Suppress the incoming request's Host header. The proxy request will default to the host name specified in the destination server address. See [RequestHeaderOriginalHost](#requestheaderoriginalhost) below. -- X-Forwarded-For - Sets the client's IP address to the X-Forwarded-For header. See [X-Forwarded](#x-forwarded) below. -- X-Forwarded-Proto - Sets the request's original scheme (http/https) to the X-Forwarded-Proto header. See [X-Forwarded](#x-forwarded) below. -- X-Forwarded-Host - Sets the request's original Host to the X-Forwarded-Host header. See [X-Forwarded](#x-forwarded) below. -- X-Forwarded-Prefix - Sets the request's original PathBase, if any, to the X-Forwarded-Prefix header. See [X-Forwarded](#x-forwarded) below. +- Host - Suppress the incoming request's Host header. The proxy request will default to the host name specified in the destination server address. See `RequestHeaderOriginalHost` below. +- X-Forwarded-For - Sets the client's IP address to the X-Forwarded-For header. See `X-Forwarded` below. +- X-Forwarded-Proto - Sets the request's original scheme (http/https) to the X-Forwarded-Proto header. See `X-Forwarded` below. +- X-Forwarded-Host - Sets the request's original Host to the X-Forwarded-Host header. See `X-Forwarded` below. +- X-Forwarded-Prefix - Sets the request's original PathBase, if any, to the X-Forwarded-Prefix header. See `X-Forwarded` below. For example the following incoming request to `http://IncomingHost:5000/path`: ``` @@ -45,7 +45,9 @@ X-Forwarded-Host: IncomingHost:5000 ## Transform Categories -Transforms fall into a few categories: request, response, and response trailers. Request trailers are not supported because they are not supported by the underlying HttpClient. +Transforms fall into a few categories: [Request](./transforms-request.md), [Response](./transforms-response.md), and [Response Trailers](./transforms-response.md#responsetrailer). Request trailers are not supported because they are not supported by the underlying HttpClient. + +If the built-in set of transforms is insufficient, then custom transforms can be added via [extensibility](./extensibility-transforms.md). ## Adding transforms @@ -116,7 +118,7 @@ All configuration entries are treated as case-insensitive, though the destinatio The details for these transforms are covered later in this document. -Developers that want to integrate their custom transforms with the `Transforms` section of configuration can do so using [ITransformFactory](#itransformfactory) described below. +Developers that want to integrate their custom transforms with the `Transforms` section of configuration can do so using `ITransformFactory` described below. ### From Code @@ -147,1069 +149,4 @@ services.AddReverseProxy() }); ``` -For more advanced control see [ITransformProvider](#itransformprovider) described below. - -## Request transforms - -Request transforms include the request path, query, HTTP version, method, and headers. In code these are represented by the [RequestTransformContext](xref:Yarp.ReverseProxy.Transforms.RequestTransformContext) object and processed by implementations of the abstract class [RequestTransform](xref:Yarp.ReverseProxy.Transforms.RequestTransform). - -Notes: -- The proxy request scheme (http/https), authority, and path base, are taken from the destination server address (`https://localhost:10001/Path/Base` in the example above) and should not be modified by transforms. -- The Host header can be overridden by transforms independent of the authority, see [RequestHeader](#requestheader) below. -- The request's original PathBase property is not used when constructing the proxy request, see [X-Forwarded](#x-forwarded). -- All incoming request headers are copied to the proxy request by default with the exception of the Host header (see [Defaults](#defaults)). [X-Forwarded](#x-forwarded) headers are also added by default. These behaviors can be configured using the following transforms. Additional request headers can be specified, or request headers can be excluded by setting them to an empty value. - -The following are built in transforms identified by their primary config key. These transforms are applied in the order they are specified in the route configuration. - -### PathPrefix - -**Modifies the request path adding a prefix value** - -| Key | Value | Required | -|-----|-------|----------| -| PathPrefix | A path starting with a '/' | yes | - -Config: -```JSON -{ "PathPrefix": "/prefix" } -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformPathPrefix(prefix: "/prefix"); -``` -```C# -transformBuilderContext.AddPathPrefix(prefix: "/prefix"); -``` -Example:
-`/request/path` becomes `/prefix/request/path` - -This will prefix the request path with the given value. - -### PathRemovePrefix - -**Modifies the request path removing a prefix value** - -| Key | Value | Required | -|-----|-------|----------| -| PathRemovePrefix | A path starting with a '/' | yes | - -Config: -```JSON -{ "PathRemovePrefix": "/prefix" } -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformPathRemovePrefix(prefix: "/prefix"); -``` -```csharp -transformBuilderContext.AddPathRemovePrefix(prefix: "/prefix"); -``` -Example:
-`/prefix/request/path` becomes `/request/path`
-`/prefix2/request/path` is not modified
- -This will remove the matching prefix from the request path. Matches are made on path segment boundaries (`/`). If the prefix does not match then no changes are made. - -### PathSet - -**Replaces the request path with the specified value** - -| Key | Value | Required | -|-----|-------|----------| -| PathSet | A path starting with a '/' | yes | - -Config: -```JSON -{ "PathSet": "/newpath" } -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformPathSet(path: "/newpath"); -``` -```C# -transformBuilderContext.AddPathSet(path: "/newpath"); -``` -Example:
-`/request/path` becomes `/newpath` - -This will set the request path with the given value. - -### PathPattern - -**Replaces the request path using a pattern template** - -| Key | Value | Required | -|-----|-------|----------| -| PathPattern | A path template starting with a '/' | yes | - -Config: -```JSON -{ "PathPattern": "/my/{plugin}/api/{**remainder}" } -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformPathRouteValues(pattern: new PathString("/my/{plugin}/api/{**remainder}")); -``` -```C# -transformBuilderContext.AddPathRouteValues(pattern: new PathString("/my/{plugin}/api/{**remainder}")); -``` - -This will set the request path with the given value and replace any `{}` segments with the associated route value. `{}` segments without a matching route value are removed. The final `{}` segment can be marked as `{**remainder}` to indicate this is a catch-all segment that may contain multiple path segments. See ASP.NET Core's [routing docs](/aspnet/core/fundamentals/routing#route-template-reference) for more information about route templates. - -Example: - -| Step | Value | -|------|-------| -| Route definition | `/api/{plugin}/stuff/{**remainder}` | -| Request path | `/api/v1/stuff/more/stuff` | -| Plugin value | `v1` | -| Remainder value | `more/stuff` | -| PathPattern | `/my/{plugin}/api/{**remainder}` | -| Result | `/my/v1/api/more/stuff` | - -### QueryValueParameter - -**Adds or replaces parameters in the request query string** - -| Key | Value | Required | -|-----|-------|----------| -| QueryValueParameter | Name of a query string parameter | yes | -| Set/Append | Static value | yes | - -Config: -```JSON -{ - "QueryValueParameter": "foo", - "Append": "bar" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformQueryValue(queryKey: "foo", value: "bar", append: true); -``` -```C# -transformBuilderContext.AddQueryValue(queryKey: "foo", value: "bar", append: true); -``` - -This will add a query string parameter with the name `foo` and sets it to the static value `bar`. - -Example: - -| Step | Value | -|------|-------| -| Query | `?a=b` | -| QueryValueParameter | `foo` | -| Append | `remainder` | -| Result | `?a=b&foo=remainder` | - -### QueryRouteParameter - -**Adds or replaces a query string parameter with a value from the route configuration** - -| Key | Value | Required | -|-----|-------|----------| -| QueryRouteParameter | Name of a query string parameter | yes | -| Set/Append | The name of a route value | yes | - -Config: -```JSON -{ - "QueryRouteParameter": "foo", - "Append": "remainder" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformQueryRouteValue(queryKey: "foo", routeValueKey: "remainder", append: true); -``` -```C# -transformBuilderContext.AddQueryRouteValue(queryKey: "foo", routeValueKey: "remainder", append: true); -``` - -This will add a query string parameter with the name `foo` and sets it to the value of the associated route value. - -Example: - -| Step | Value | -|------|-------| -| Route definition | `/api/{*remainder}` | -| Request path | `/api/more/stuff` | -| Remainder value | `more/stuff` | -| QueryRouteParameter | `foo` | -| Append | `remainder` | -| Result | `?foo=more/stuff` | - -### QueryRemoveParameter - -**Removes the specified parameter from the request query string** - -| Key | Value | Required | -|-----|-------|----------| -| QueryRemoveParameter | Name of a query string parameter | yes | - -Config: -```JSON -{ "QueryRemoveParameter": "foo" } -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformQueryRemoveKey(queryKey: "foo"); -``` -```C# -transformBuilderContext.AddQueryRemoveKey(queryKey: "foo"); -``` - -This will remove a query string parameter with the name `foo` if present on the request. - -Example: - -| Step | Value | -|------|-------| -| Request path | `?a=b&foo=c` | -| QueryRemoveParameter | `foo` | -| Result | `?a=b` | - -### HttpMethodChange - -**Changes the http method used in the request** - -| Key | Value | Required | -|-----|-------|----------| -| HttpMethodChange | The http method to replace | yes | -| Set | The new http method | yes | - -Config: -```JSON -{ - "HttpMethodChange": "PUT", - "Set": "POST" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformHttpMethodChange(fromHttpMethod: HttpMethods.Put, toHttpMethod: HttpMethods.Post); -``` -```C# -transformBuilderContext.AddHttpMethodChange(fromHttpMethod: HttpMethods.Put, toHttpMethod: HttpMethods.Post); -``` - -This will change PUT requests to POST. - -### RequestHeadersCopy - -**Sets whether incoming request headers are copied to the outbound request** - -| Key | Value | Default | Required | -|-----|-------|---------|----------| -| RequestHeadersCopy | true/false | true | yes | - -Config: -```JSON -{ "RequestHeadersCopy": "false" } -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformCopyRequestHeaders(copy: false); -``` -```C# -transformBuilderContext.CopyRequestHeaders = false; -``` - -This sets if all incoming request headers are copied to the proxy request. This setting is enabled by default and can by disabled by configuring the transform with a `false` value. Transforms that reference specific headers will still be run if this is disabled. - -### RequestHeaderOriginalHost - -**Specifies if the incoming request Host header should be copied to the proxy request** - -| Key | Value | Default | Required | -|-----|-------|---------|----------| -| RequestHeaderOriginalHost | true/false | false | yes | - -Config: -```JSON -{ "RequestHeaderOriginalHost": "true" } -``` -```csharp -routeConfig = routeConfig.WithTransformUseOriginalHostHeader(useOriginal: true); -``` -```C# -transformBuilderContext.AddOriginalHost(true); -``` - -This specifies if the incoming request Host header should be copied to the proxy request. This setting is disabled by default and can be enabled by configuring the transform with a `true` value. Transforms that directly reference the `Host` header will override this transform. - -### RequestHeader - -**Adds or replaces request headers** - -| Key | Value | Required | -|-----|-------|----------| -| RequestHeader | The header name | yes | -| Set/Append | The header value | yes | - -Config: -```JSON -{ - "RequestHeader": "MyHeader", - "Set": "MyValue" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformRequestHeader(headerName: "MyHeader", value: "MyValue", append: false); -``` -```C# -transformBuilderContext.AddRequestHeader(headerName: "MyHeader", value: "MyValue", append: false); -``` - -Example: -``` -MyHeader: MyValue -``` - -This sets or appends the value for the named header. Set replaces any existing header. Append adds an additional header with the given value. -Note: setting "" as a header value is not recommended and can cause an undefined behavior. - -### RequestHeaderRouteValue - -**Adds or replaces a header with a value from the route configuration** - -| Key | Value | Required | -|-----|-------|----------| -| RequestHeader | Name of a query string parameter | yes | -| Set/Append | The name of a route value | yes | - -Config: -```JSON -{ - "RequestHeaderRouteValue": "MyHeader", - "Set": "MyRouteKey" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformRequestHeaderRouteValue(headerName: "MyHeader", routeValueKey: "key", append: false); -``` -```C# -transformBuilderContext.AddRequestHeaderRouteValue(headerName: "MyHeader", routeValueKey: "key", append: false); -``` - -Example: - -| Step | Value | -|------|---------------------| -| Route definition | `/api/{*remainder}` | -| Request path | `/api/more/stuff` | -| Remainder value | `more/stuff` | -| RequestHeaderFromRoute | `foo` | -| Append | `remainder` | -| Result | `foo: more/stuff` | - -This sets or appends the value for the named header with a value from the route configuration. Set replaces any existing header. Append adds an additional header with the given value. -Note: setting "" as a header value is not recommended and can cause an undefined behavior. - -### RequestHeaderRemove - -**Removes request headers** - -| Key | Value | Required | -|-----|-------|----------| -| RequestHeaderRemove | The header name | yes | - -Config: -```JSON -{ - "RequestHeaderRemove": "MyHeader" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformRequestHeaderRemove(headerName: "MyHeader"); -``` -```C# -transformBuilderContext.AddRequestHeaderRemove(headerName: "MyHeader"); -``` - -Example: -``` -MyHeader: MyValue -AnotherHeader: AnotherValue -``` - -This removes the named header. - -### RequestHeadersAllowed - -| Key | Value | Required | -|-----|-------|----------| -| RequestHeadersAllowed | A semicolon separated list of allowed header names. | yes | - -Config: -```JSON -{ - "RequestHeadersAllowed": "Header1;header2" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformRequestHeadersAllowed("Header1", "header2"); -``` -```C# -transformBuilderContext.AddRequestHeadersAllowed("Header1", "header2"); -``` - -YARP copies most request headers to the proxy request by default (see [RequestHeadersCopy](transforms.md#requestheaderscopy)). Some security models only allow specific headers to be proxied. This transform disables RequestHeadersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list. - -Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities. - -Example: -``` -Header1: value1 -Header2: value2 -AnotherHeader: AnotherValue -``` - -Only header1 and header2 are copied to the proxy request. - -### X-Forwarded - -**Adds headers with information about the original client request** - -| Key | Value | Default | Required | -|-----|-------|---------|----------| -| X-Forwarded | Default action (Set, Append, Remove, Off) to apply to all X-Forwarded-* listed below | Set | yes | -| For | Action to apply to this header | * See X-Forwarded | no | -| Proto | Action to apply to this header | * See X-Forwarded | no | -| Host | Action to apply to this header | * See X-Forwarded | no | -| Prefix | Action to apply to this header | * See X-Forwarded | no | -| HeaderPrefix | The header name prefix | "X-Forwarded-" | no | - -Action "Off" completely disables the transform. - -Config: -```JSON -{ - "X-Forwarded": "Set", - "For": "Remove", - "Proto": "Append", - "Prefix": "Off", - "HeaderPrefix": "X-Forwarded-" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformXForwarded( - headerPrefix = "X-Forwarded-", - ForwardedTransformActions xDefault = ForwardedTransformActions.Set, - ForwardedTransformActions? xFor = null, - ForwardedTransformActions? xHost = null, - ForwardedTransformActions? xProto = null, - ForwardedTransformActions? xPrefix = null); -``` -```C# -transformBuilderContext.AddXForwarded(ForwardedTransformActions.Set); -transformBuilderContext.AddXForwardedFor(headerName: "X-Forwarded-For", ForwardedTransformActions.Append); -transformBuilderContext.AddXForwardedHost(headerName: "X-Forwarded-Host", ForwardedTransformActions.Append); -transformBuilderContext.AddXForwardedProto(headerName: "X-Forwarded-Proto", ForwardedTransformActions.Off); -transformBuilderContext.AddXForwardedPrefix(headerName: "X-Forwarded-Prefix", ForwardedTransformActions.Remove); -``` - -Example: -``` -X-Forwarded-For: 5.5.5.5 -X-Forwarded-Proto: https -X-Forwarded-Host: IncomingHost:5000 -X-Forwarded-Prefix: /path/base -``` -Disable default headers: -```JSON -{ "X-Forwarded": "Off" } -``` -```C# -transformBuilderContext.UseDefaultForwarders = false; -``` - -When the proxy connects to the destination server, the connection is indepenent from the one the client made to the proxy. The destination server likely needs original connection information for security checks and to properly generate absolute URIs for links and redirects. To enable information about the client connection to be passed to the destination a set of extra headers can be added. Until the `Forwarded` standard was created, a common solution is to use `X-Forwarded-*` headers. There is no official standard that defines the `X-Forwarded-*` headers and implementations vary, check your destination server for support. - -This transform is enabled by default even if not specified in the route config. - -Set the `X-Forwarded` value to a comma separated list containing the headers you need to enable. All for headers are enabled by default. All can be disabled by specifying the value `"Off"`. - - -The Prefix specifies the header name prefix to use for each header. With the default `X-Forwarded-` prefix the resulting headers will be `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-Prefix`. - -Transform action specifies how each header should be combined with an existing header of the same name. It can be "Set", "Append", "Remove, or "Off" (completely disable the transform). A request traversing multiple proxies may accumulate a list of such headers and the destination server will need to evaluate the list to determine the original value. If action is "Set" and the associated value is not available on the request (e.g. RemoteIpAddress is null), any existing header is still removed to prevent spoofing. - -The {Prefix}For header value is taken from `HttpContext.Connection.RemoteIpAddress` representing the prior caller's IP address. The port is not included. IPv6 addresses do not include the bounding `[]` brackets. - -The {Prefix}Proto header value is taken from `HttpContext.Request.Scheme` indicating if the prior caller used HTTP or HTTPS. - -The {Prefix}Host header value is taken from the incoming request's Host header. This is independent of RequestHeaderOriginalHost specified above. Unicode/IDN hosts are punycode encoded. - -The {Prefix}Prefix header value is taken from `HttpContext.Request.PathBase`. The PathBase property is not used when generating the proxy request so the destination server will need the original value to correctly generate links and directs. The value is in the percent encoded Uri format. - -### Forwarded - -**Adds a header with information about the original client request** - -| Key | Value | Default | Required | -|-----|-------|---------|----------| -| Forwarded | A comma separated list containing any of these values: for,by,proto,host | (none) | yes | -| ForFormat | Random/RandomAndPort/RandomAndRandomPort/Unknown/UnknownAndPort/UnknownAndRandomPort/Ip/IpAndPort/IpAndRandomPort | Random | no | -| ByFormat | Random/RandomAndPort/RandomAndRandomPort/Unknown/UnknownAndPort/UnknownAndRandomPort/Ip/IpAndPort/IpAndRandomPort | Random | no | -| Action | Action to apply to this header (Set, Append, Remove, Off) | Set | no | - -Config: -```JSON -{ - "Forwarded": "by,for,host,proto", - "ByFormat": "Random", - "ForFormat": "IpAndPort", - "Action": "Append" -}, -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformForwarded(useHost: true, useProto: true, forFormat: NodeFormat.IpAndPort, ByFormat: NodeFormat.Random, action: ForwardedTransformAction.Append); -``` -```C# -transformBuilderContext.AddForwarded(useHost: true, useProto: true, forFormat: NodeFormat.IpAndPort, ByFormat: NodeFormat.Random, action: ForwardedTransformAction.Append); -``` -Example: -``` -Forwarded: proto=https;host="localhost:5001";for="[::1]:20173";by=_YQuN68tm6 -``` - -The `Forwarded` header is defined by [RFC 7239](https://tools.ietf.org/html/rfc7239). It consolidates many of the same functions as the unofficial X-Forwarded headers, flowing information to the destination server that would otherwise be obscured by using a proxy. - -Enabling this transform will disable the default X-Forwarded transforms as they carry similar information in another format. The X-Forwarded transforms can still be explicitly enabled. - -Action: This specifies how the transform should handle an existing Forwarded header. It can be "Set", "Append", "Remove, or "Off" (completely disable the transform). A request traversing multiple proxies may accumulate a list of such headers and the destination server will need to evaluate the list to determine the original value. - -Proto: This value is taken from `HttpContext.Request.Scheme` indicating if the prior caller used HTTP or HTTPS. - -Host: This value is taken from the incoming request's Host header. This is independent of RequestHeaderOriginalHost specified above. Unicode/IDN hosts are punycode encoded. - -For: This value identifies the prior caller. IP addresses are taken from `HttpContext.Connection.RemoteIpAddress`. See ByFormat and ForFormat below for details. - -By: This value identifies where the proxy received the request. IP addresses are taken from `HttpContext.Connection.LocalIpAddress`. See ByFormat and ForFormat below for details. - -ByFormat and ForFormat: - -The RFC allows a [variety of formats](https://tools.ietf.org/html/rfc7239#section-6) for the By and For fields. It requires that the default format uses an obfuscated identifier identified here as Random. - -| Format | Description | Example | -|--------|-------------|---------| -| Random | An obfuscated identifier that is generated randomly per request. This allows for diagnostic tracing scenarios while limiting the flow of uniquely identifying information for privacy reasons. | `by=_YQuN68tm6` | -| RandomAndPort | The Random identifier plus the port. | `by="_YQuN68tm6:80"` | -| RandomAndRandomPort | The Random identifier plus another random identifier for the port. | `by="_YQuN68tm6:_jDw5Cf3tQ"` | -| Unknown | This can be used when the identity of the preceding entity is not known, but the proxy server still wants to signal that the request was forwarded. | `by=unknown` | -| UnknownAndPort | The Unknown identifier plus the port if available. | `by="unknown:80"` | -| UnknownAndRandomPort | The Unknown identifier plus random identifier for the port. | `by="unknown:_jDw5Cf3tQ"` | -| Ip | An IPv4 address or an IPv6 address including brackets. | `by="[::1]"` | -| IpAndPort | The IP address plus the port. | `by="[::1]:80"` | -| IpAndRandomPort | The IP address plus random identifier for the port. | `by="[::1]:_jDw5Cf3tQ"` | - -### ClientCert - -**Forwards the client cert used on the inbound connection as a header to destination** - -| Key | Value | Required | -|-----|-------|----------| -| ClientCert | The header name | yes | - -Config: -```JSON -{ "ClientCert": "X-Client-Cert" } -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformClientCertHeader(headerName: "X-Client-Cert"); -``` -```C# -transformBuilderContext.AddClientCertHeader(headerName: "X-Client-Cert"); -``` -Example: -``` -X-Client-Cert: SSdtIGEgY2VydGlmaWNhdGU... -``` -As the inbound and outbound connections are independent, there needs to be a way to pass any inbound client certificate to the destination server. This transform causes the client certificate taken from `HttpContext.Connection.ClientCertificate` to be Base64 encoded and set as the value for the given header name. The destination server may need that certificate to authenticate the client. There is no standard that defines this header and implementations vary, check your destination server for support. - -Servers do minimal validation on the incoming client certificate by default. The certificate should be validated either in the proxy or the destination, see the [client certificate auth](/aspnet/core/security/authentication/certauth) docs for details. - -This transform will only apply if the client certificate is already present on the connection. See the [optional certs doc](/aspnet/core/security/authentication/certauth#optional-client-certificates) if it needs to be requested from the client on a per-route basis. - -## Response and Response Trailers - -All response headers and trailers are copied from the proxied response to the outgoing client response by default. Response and response trailer transforms may specify if they should be applied only for successful responses or for all responses. - -In code these are implemented as derivations of the abstract classes [ResponseTransform](xref:Yarp.ReverseProxy.Transforms.ResponseTransform) and [ResponseTrailersTransform](xref:Yarp.ReverseProxy.Transforms.ResponseTrailersTransform). - -### ResponseHeadersCopy - -**Sets whether destination response headers are copied to the client** - -| Key | Value | Default | Required | -|-----|-------|---------|----------| -| ResponseHeadersCopy | true/false | true | yes | - -Config: -```JSON -{ "ResponseHeadersCopy": "false" } -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformCopyResponseHeaders(copy: false); -``` -```C# -transformBuilderContext.CopyResponseHeaders = false; -``` - -This sets if all proxy response headers are copied to the client response. This setting is enabled by default and can be disabled by configuring the transform with a `false` value. Transforms that reference specific headers will still be run if this is disabled. - -### ResponseHeader - -**Adds or replaces response headers** - -| Key | Value | Default | Required | -|-----|-------|---------|----------| -| ResponseHeader | The header name | (none) | yes | -| Set/Append | The header value | (none) | yes | -| When | Success/Always/Failure | Success | no | - -Config: -```JSON -{ - "ResponseHeader": "HeaderName", - "Append": "value", - "When": "Success" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformResponseHeader(headerName: "HeaderName", value: "value", append: true, ResponseCondition.Success); -``` -```C# -transformBuilderContext.AddResponseHeader(headerName: "HeaderName", value: "value", append: true, always: ResponseCondition.Success); -``` -Example: -``` -HeaderName: value -``` - -This sets or appends the value for the named response header. Set replaces any existing header. Append adds an additional header with the given value. -Note: setting "" as a header value is not recommended and can cause an undefined behavior. - -`When` specifies if the response header should be included for all, successful, or failure responses. Any response with a status code less than 400 is considered a success. - -### ResponseHeaderRemove - -**Removes response headers** - -| Key | Value | Default | Required | -|-----|-------|---------|----------| -| ResponseHeaderRemove | The header name | (none) | yes | -| When | Success/Always/Failure | Success | no | - -Config: -```JSON -{ - "ResponseHeaderRemove": "HeaderName", - "When": "Success" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformResponseHeaderRemove(headerName: "HeaderName", ResponseCondition.Success); -``` -```C# -transformBuilderContext.AddResponseHeaderRemove(headerName: "HeaderName", ResponseCondition.Success); -``` -Example: -``` -HeaderName: value -AnotherHeader: another-value -``` - -This removes the named response header. - -`When` specifies if the response header should be removed for all, successful, or failure responses. Any response with a status code less than 400 is considered a success. - -### ResponseHeadersAllowed - -| Key | Value | Required | -|-----|-------|----------| -| ResponseHeadersAllowed | A semicolon separated list of allowed header names. | yes | - -Config: -```JSON -{ - "ResponseHeadersAllowed": "Header1;header2" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformResponseHeadersAllowed("Header1", "header2"); -``` -```C# -transformBuilderContext.AddResponseHeadersAllowed("Header1", "header2"); -``` - -YARP copies most response headers from the proxy response by default (see [ResponseHeadersCopy](transforms.md#responseheaderscopy)). Some security models only allow specific headers to be proxied. This transform disables ResponseHeadersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list. - -Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities. - -Example: -``` -Header1: value1 -Header2: value2 -AnotherHeader: AnotherValue -``` - -Only header1 and header2 are copied from the proxy response. - -### ResponseTrailersCopy - -**Sets whether destination trailing response headers are copied to the client** - -| Key | Value | Default | Required | -|-----|-------|---------|----------| -| ResponseTrailersCopy | true/false | true | yes | - -Config: -```JSON -{ "ResponseTrailersCopy": "false" } -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformCopyResponseTrailers(copy: false); -``` -```C# -transformBuilderContext.CopyResponseTrailers = false; -``` - -This sets if all proxy response trailers are copied to the client response. This setting is enabled by default and can be disabled by configuring the transform with a `false` value. Transforms that reference specific headers will still be run if this is disabled. - -### ResponseTrailer - -**Adds or replaces trailing response headers** - -| Key | Value | Default | Required | -|-----|-------|---------|----------| -| ResponseTrailer | The header name | (none) | yes | -| Set/Append | The header value | (none) | yes | -| When | Success/Always/Failure | Success | no | - -Config: -```JSON -{ - "ResponseTrailer": "HeaderName", - "Append": "value", - "When": "Success" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformResponseTrailer(headerName: "HeaderName", value: "value", append: true, ResponseCondition.Success); -``` -```C# -transformBuilderContext.AddResponseTrailer(headerName: "HeaderName", value: "value", append: true, ResponseCondition.Success); -``` -Example: -``` -HeaderName: value -``` - -Response trailers are headers sent at the end of the response body. Support for trailers is uncommon in HTTP/1.1 implementations but is becoming common in HTTP/2 implementations. Check your client and server for support. - -ResponseTrailer follows the same structure and guidance as [ResponseHeader](transforms.md#responseheader). - -### ResponseTrailerRemove - -**Removes trailing response headers** - -| Key | Value | Default | Required | -|-----|-------|---------|----------| -| ResponseTrailerRemove | The header name | (none) | yes | -| When | Success/Always/Failure | Success | no | - -Config: -```JSON -{ - "ResponseTrailerRemove": "HeaderName", - "When": "Success" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformResponseTrailerRemove(headerName: "HeaderName", ResponseCondition.Success); -``` -```C# -transformBuilderContext.AddResponseTrailerRemove(headerName: "HeaderName", ResponseCondition.Success); -``` -Example: -``` -HeaderName: value -AnotherHeader: another-value -``` - -This removes the named trailing header. - -ResponseTrailerRemove follows the same structure and guidance as [ResponseHeaderRemove](transforms.md#responseheaderremove). - -### ResponseTrailersAllowed - -| Key | Value | Required | -|-----|-------|----------| -| ResponseTrailersAllowed | A semicolon separated list of allowed header names. | yes | - -Config: -```JSON -{ - "ResponseTrailersAllowed": "Header1;header2" -} -``` -Code: -```csharp -routeConfig = routeConfig.WithTransformResponseTrailersAllowed("Header1", "header2"); -``` -```C# -transformBuilderContext.AddResponseTrailersAllowed("Header1", "header2"); -``` - -YARP copies most response trailers from the proxy response by default (see [ResponseTrailersCopy](transforms.md#responsetrailerscopy)). Some security models only allow specific headers to be proxied. This transform disables ResponseTrailersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list. - -Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities. - -Example: -``` -Header1: value1 -Header2: value2 -AnotherHeader: AnotherValue -``` - -Only header1 and header2 are copied from the proxy response. - -## Extensibility - -### AddRequestTransform - -[AddRequestTransform](xref:Yarp.ReverseProxy.Transforms.TransformBuilderContextFuncExtensions.AddRequestTransform*) is a `TransformBuilderContext` extension method that defines a request transform as a `Func`. This allows creating a custom request transform without implementing a `RequestTransform` derived class. - -### AddResponseTransform - -[AddResponseTransform](xref:Yarp.ReverseProxy.Transforms.TransformBuilderContextFuncExtensions.AddResponseTransform*) is a `TransformBuilderContext` extension method that defines a response transform as a `Func`. This allows creating a custom response transform without implementing a `ResponseTransform` derived class. - -### AddResponseTrailersTransform - -[AddResponseTrailersTransform](xref:Yarp.ReverseProxy.Transforms.TransformBuilderContextFuncExtensions.AddResponseTrailersTransform*) is a `TransformBuilderContext` extension method that defines a response trailers transform as a `Func`. This allows creating a custom response trailers transform without implementing a `ResponseTrailersTransform` derived class. - -### RequestTransform - -All request transforms must derive from the abstract base class [RequestTransform](xref:Yarp.ReverseProxy.Transforms.RequestTransform). These can freely modify the proxy `HttpRequestMessage`. Avoid reading or modifying the request body as this may disrupt the proxying flow. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and easy of use. - -A request transform may conditionally produce an immediate response such as for error conditions. This prevents any remaining transforms from running and the request from being proxied. This is indicated by setting the `HttpResponse.StatusCode` to a value other than 200, or calling `HttpResponse.StartAsync()`, or writing to the `HttpResponse.Body` or `BodyWriter`. - -### ResponseTransform - -All response transforms must derive from the abstract base class [ResponseTransform](xref:Yarp.ReverseProxy.Transforms.ResponseTransform). These can freely modify the client `HttpResponse`. Avoid reading or modifying the response body as this may disrupt the proxying flow. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and easy of use. - -### ResponseTrailersTransform - -All response trailers transforms must derive from the abstract base class [ResponseTrailersTransform](xref:Yarp.ReverseProxy.Transforms.ResponseTrailersTransform). These can freely modify the client HttpResponse trailers. These run after the response body and should not attempt to modify the response headers or body. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and easy of use. - -### Request body transforms - -YARP does not provide any built in transforms for modifying the request body. However, the body can be modified in custom transforms. - -Be careful about which kinds of requests are modified, how much data gets buffered, enforcing timeouts, parsing untrusted input, and updating the body-related headers like `Content-Length`. - -The below example uses simple, inefficient buffering to transform requests. A more efficient implementation would wrap and replace `HttpContext.Request.Body` with a stream that performed the needed modifications as data was proxied from client to server. That would also require removing the Content-Length header since the final length would not be known in advance. - -This sample requires YARP 1.1, see https://github.com/microsoft/reverse-proxy/pull/1569. - -```C# -.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; - } - }); -}); -``` - -### Response body transforms - -YARP does not provide any built in transforms for modifying the response body. However, the body can be modified in custom transforms. - -Be careful about which kinds of responses are modified, how much data gets buffered, enforcing timeouts, parsing untrusted input, and updating the body-related headers like `Content-Length`. You may need to decompress content before modifying it, as indicated by the Content-Encoding header, and afterwards re-compress it or remove the header. - -The below example uses simple, inefficient buffering to transform responses. A more efficient implementation would wrap the stream returned by `ReadAsStreamAsync()` with a stream that performed the needed modifications as data was proxied from client to server. That would also require removing the Content-Length header since the final length would not be known in advance. - -```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](xref:Yarp.ReverseProxy.Transforms.Builder.ITransformProvider) provides the functionality of `AddTransforms` described above as well as DI integration and validation support. - -`ITransformProvider`'s can be registered in DI by calling [AddTransforms<T>()](xref:Microsoft.Extensions.DependencyInjection.ReverseProxyServiceCollectionExtensions). Multiple `ITransformProvider` implementations can be registered and all will be run. - -`ITransformProvider` has two methods, `Validate` and `Apply`. `Validate` gives you the opportunity to inspect the route for any parameters that are needed to configure a transform, such as custom metadata, and to return validation errors on the context if any needed values are missing or invalid. The `Apply` method provides the same functionality as AddTransform as discussed above, adding and configuring transforms per route. - -```C# -services.AddReverseProxy() - .LoadFromConfig(_configuration.GetSection("ReverseProxy")) - .AddTransforms(); -``` -```C# -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("CustomMetadata"), value); - return default; - }); - } - } -} -``` - -### ITransformFactory - -Developers that want to integrate their custom transforms with the `Transforms` section of configuration can implement an [ITransformFactory](xref:Yarp.ReverseProxy.Transforms.Builder.ITransformFactory). This should be registered in DI using the `AddTransformFactory()` method. Multiple factories can be registered and all will be used. - -`ITransformFactory` provides two methods, `Validate` and `Build`. These process one set of transform values at a time, represented by a `IReadOnlyDictionary`. - -The `Validate` method is called when loading a configuration to verify the contents and report all errors. Any reported errors will prevent the configuration from being applied. - -The `Build` method takes the given configuration and produces the associated transform instances for the route. - -```C# -services.AddReverseProxy() - .LoadFromConfig(_configuration.GetSection("ReverseProxy")) - .AddTransformFactory(); -``` -```C# -internal class MyTransformFactory : ITransformFactory -{ - public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary 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 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("CustomTransform"), value); - return default; - }); - - return true; - } - - return false; - } -} -``` - -`Validate` and `Build` return `true` if they've identified the given transform configuration as one that they own. A `ITransformFactory` may implement multiple transforms. Any `RouteConfig.Transforms` entries not handled by any `ITransformFactory` will be considered configuration errors and prevent the configuration from being applied. - -Consider also adding parametrized extension methods on `RouteConfig` like `WithTransformQueryValue` to facilitate programmatic route construction. - -```C# -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; - }); -} -``` +For more advanced control see `ITransformProvider` described below. diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index ce4f7b10c395..cc8490279aaa 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -1205,8 +1205,16 @@ items: displayName: yarp uid: fundamentals/servers/yarp/queryparameter-routing - name: Transforms - displayName: yarp - uid: fundamentals/servers/yarp/transforms + items: + - name: Transforms Overview + displayName: yarp + uid: fundamentals/servers/yarp/transforms + - name: Request Transforms + displayName: yarp + uid: fundamentals/servers/yarp/transforms-request + - name: Response Transforms + displayName: yarp + uid: fundamentals/servers/yarp/transforms-response - name: Load Balancing displayName: yarp uid: fundamentals/servers/yarp/load-balancing @@ -1271,6 +1279,9 @@ items: - name: Destination Resolvers displayName: yarp uid: fundamentals/servers/yarp/destination-resolvers + - name: Request and Response Transform Extensibility + displayName: yarp + uid: fundamentals/servers/yarp/transform-extensibility - name: Deployment options items: - name: HTTP.sys Delegation