Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Request Header From Route #2262

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/docfx/articles/transforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,44 @@ 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.

### RequestHeaderFromRoute
Tratcher marked this conversation as resolved.
Show resolved Hide resolved

**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
{
"RequestHeaderFromRoute": "foo",
"Set": "remainder"
}
```
Code:
```csharp
routeConfig = routeConfig.WithTransformRequestHeaderFromRoute(headerName: "MyHeader", routeValueKey: "key", append: false);
```
```C#
transformBuilderContext.AddRequestHeaderFromRoute(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**
Expand Down
36 changes: 36 additions & 0 deletions src/ReverseProxy/Transforms/RequestHeaderFromRouteTransform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;

namespace Yarp.ReverseProxy.Transforms;

public class RequestHeaderFromRouteTransform : RequestHeaderTransform
{
public RequestHeaderFromRouteTransform(RequestHeaderTransformMode mode, string headerName, string routeValueKey)
: base(mode, headerName)
{
if (string.IsNullOrEmpty(headerName))
{
throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName));
}

if (string.IsNullOrEmpty(routeValueKey))
{
throw new ArgumentException($"'{nameof(routeValueKey)}' cannot be null or empty.", nameof(routeValueKey));
}

RouteValueKey = routeValueKey;
}

internal string RouteValueKey { get; }

protected override string? GetValue(RequestTransformContext context)
{
var routeValues = context.HttpContext.Request.RouteValues;
if (!routeValues.TryGetValue(RouteValueKey, out var value))
{
return null;
}

return value?.ToString();
}
}

62 changes: 62 additions & 0 deletions src/ReverseProxy/Transforms/RequestHeaderTransform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;

namespace Yarp.ReverseProxy.Transforms;

public enum RequestHeaderTransformMode
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
{
Append,
Set
}

public abstract class RequestHeaderTransform : RequestTransform
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
{
protected RequestHeaderTransform(RequestHeaderTransformMode mode, string headerName)
{
if (string.IsNullOrEmpty(headerName))
{
throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName));
}

Mode = mode;
HeaderName = headerName;
}

internal RequestHeaderTransformMode Mode { get; }
internal string HeaderName { get; }

public override ValueTask ApplyAsync(RequestTransformContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

var value = GetValue(context);
if (value is null)
{
return default;
}

switch (Mode)
{
case RequestHeaderTransformMode.Append:
var existingValues = TakeHeader(context, HeaderName);
var newValue = StringValues.Concat(existingValues, value);
AddHeader(context, HeaderName, newValue);
break;
case RequestHeaderTransformMode.Set:
RemoveHeader(context, HeaderName);
AddHeader(context, HeaderName, value);
break;
default:
throw new NotImplementedException(Mode.ToString());
}

return default;
}

protected abstract string? GetValue(RequestTransformContext context);
}

Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ public static RouteConfig WithTransformRequestHeader(this RouteConfig route, str
});
}

/// <summary>
/// Clones the route and adds the transform which will append or set the request header.
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public static RouteConfig WithTransformRequestHeaderFromRoute(this RouteConfig route, string headerName, string routeValueKey, bool append = true)
{
var type = append ? RequestHeadersTransformFactory.AppendKey : RequestHeadersTransformFactory.SetKey;
return route.WithTransform(transform =>
{
transform[RequestHeadersTransformFactory.RequestHeaderFromRouteKey] = headerName;
transform[type] = routeValueKey;
});
}

/// <summary>
/// Clones the route and adds the transform which will remove the request header.
/// </summary>
Expand Down Expand Up @@ -78,6 +91,17 @@ public static TransformBuilderContext AddRequestHeader(this TransformBuilderCont
return context;
}

/// <summary>
/// Adds the transform which will append or set the request header from a route value.
/// </summary>
public static TransformBuilderContext AddRequestHeaderFromRoute(this TransformBuilderContext context, string headerName, string routeValueKey, bool append = true)
{
context.RequestTransforms.Add(new RequestHeaderFromRouteTransform(
append ? RequestHeaderTransformMode.Append : RequestHeaderTransformMode.Set,
headerName, routeValueKey));
return context;
}

/// <summary>
/// Adds the transform which will remove the request header.
/// </summary>
Expand Down
25 changes: 25 additions & 0 deletions src/ReverseProxy/Transforms/RequestHeadersTransformFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal sealed class RequestHeadersTransformFactory : ITransformFactory
internal static readonly string RequestHeadersCopyKey = "RequestHeadersCopy";
internal static readonly string RequestHeaderOriginalHostKey = "RequestHeaderOriginalHost";
internal static readonly string RequestHeaderKey = "RequestHeader";
internal static readonly string RequestHeaderFromRouteKey = "RequestHeaderFromRoute";
internal static readonly string RequestHeaderRemoveKey = "RequestHeaderRemove";
internal static readonly string RequestHeadersAllowedKey = "RequestHeadersAllowed";
internal static readonly string AppendKey = "Append";
Expand Down Expand Up @@ -43,6 +44,14 @@ public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionar
context.Errors.Add(new ArgumentException($"Unexpected parameters for RequestHeader: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'"));
}
}
else if (transformValues.TryGetValue(RequestHeaderFromRouteKey, out var _))
{
TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2);
if (!transformValues.TryGetValue(AppendKey, out _) && !transformValues.TryGetValue(SetKey, out _))
{
context.Errors.Add(new ArgumentException($"Unexpected parameters for RequestHeaderFromRoute: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'."));
}
}
else if (transformValues.TryGetValue(RequestHeaderRemoveKey, out var _))
{
TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);
Expand Down Expand Up @@ -87,6 +96,22 @@ public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, s
throw new ArgumentException($"Unexpected parameters for RequestHeader: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'");
}
}
else if (transformValues.TryGetValue(RequestHeaderFromRouteKey, out var requestHeaderFromRoute))
{
TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);
if (transformValues.TryGetValue(AppendKey, out var routeValueKeyAppend))
{
context.AddRequestHeaderFromRoute(requestHeaderFromRoute, routeValueKeyAppend, append: true);
}
else if (transformValues.TryGetValue(SetKey, out var routeValueKeySet))
{
context.AddRequestHeaderFromRoute(requestHeaderFromRoute, routeValueKeySet, append: false);
}
else
{
throw new NotSupportedException(string.Join(";", transformValues.Keys));
}
}
else if (transformValues.TryGetValue(RequestHeaderRemoveKey, out var removeHeaderName))
{
TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
using Xunit;
using Yarp.ReverseProxy.Transforms;

namespace Yarp.ReverseProxy.Tests.Transforms;

public class RequestHeaderFromRouteTransformTests
{
[Theory]
[InlineData("defaultHeader","value","/{a}/{b}/{c}", "a", "value;6", RequestHeaderTransformMode.Append)]
[InlineData("defaultHeader","value","/{a}/{b}/{c}", "notInRoute", "value", RequestHeaderTransformMode.Append)]
[InlineData("defaultHeader","value","/{a}/{b}/{c}", "notInRoute", "value", RequestHeaderTransformMode.Set)]
[InlineData("defaultHeader","value","/{a}/{b}/{c}", "a", "6", RequestHeaderTransformMode.Set)]
[InlineData("h1","value","/{a}/{b}/{c}", "a", "6", RequestHeaderTransformMode.Set)]
[InlineData("h1","value","/{a}/{b}/{c}", "b", "7", RequestHeaderTransformMode.Set)]
[InlineData("h1","value","/{a}/{*remainder}", "remainder", "7/8", RequestHeaderTransformMode.Set)]
public async Task AddsRequestHeaderFromRouteValue_SetHeader(string headerName, string defaultHeaderStartValue, string pattern, string routeValueKey, string expected, RequestHeaderTransformMode mode)
{
// Arrange
const string path = "/6/7/8";

var routeValues = new RouteValueDictionary();
var templateMatcher = new TemplateMatcher(TemplateParser.Parse(pattern), new RouteValueDictionary());
templateMatcher.TryMatch(path, routeValues);

var httpContext = new DefaultHttpContext();
httpContext.Request.RouteValues = routeValues;
var proxyRequest = new HttpRequestMessage();
proxyRequest.Headers.Add("defaultHeader", defaultHeaderStartValue.Split(";", StringSplitOptions.RemoveEmptyEntries));

var context = new RequestTransformContext()
{
Path = path,
HttpContext = httpContext,
ProxyRequest = proxyRequest,
HeadersCopied = true
};

// Act
var transform = new RequestHeaderFromRouteTransform(mode, headerName, routeValueKey);
await transform.ApplyAsync(context);

// Assert
Assert.Equal(expected.Split(";", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(headerName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ public void WithTransformRequestHeader(bool append)
ValidateRequestHeader(append, builderContext);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void WithTransformRequestHeaderFromRoute(bool append)
{
var routeConfig = new RouteConfig();
routeConfig = routeConfig.WithTransformRequestHeaderFromRoute("key", "value", append);

var builderContext = ValidateAndBuild(routeConfig, _factory);

ValidateHeaderRouteParameter(append, builderContext);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand All @@ -65,6 +78,17 @@ public void AddRequestHeader(bool append)
ValidateRequestHeader(append, builderContext);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void AddRequestHeaderFromRouteValue(bool append)
{
var builderContext = CreateBuilderContext();
builderContext.AddRequestHeaderFromRoute("key", "value", append);

ValidateHeaderRouteParameter(append, builderContext);
}

[Fact]
public void WithTransformRequestHeaderRemove()
{
Expand Down Expand Up @@ -115,4 +139,14 @@ private static void ValidateRequestHeader(bool append, TransformBuilderContext b
Assert.Equal("value", requestHeaderValueTransform.Value);
Assert.Equal(append, requestHeaderValueTransform.Append);
}

private static void ValidateHeaderRouteParameter(bool append, TransformBuilderContext builderContext)
{
var requestTransform = Assert.Single(builderContext.RequestTransforms);
var requestHeaderFromRouteTransform = Assert.IsType<RequestHeaderFromRouteTransform>(requestTransform);
Assert.Equal("key", requestHeaderFromRouteTransform.HeaderName);
Assert.Equal("value", requestHeaderFromRouteTransform.RouteValueKey);
var expectedMode = append ? RequestHeaderTransformMode.Append : RequestHeaderTransformMode.Set;
Assert.Equal(expectedMode, requestHeaderFromRouteTransform.Mode);
}
}