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 all commits
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.

### 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**
Expand Down
36 changes: 36 additions & 0 deletions src/ReverseProxy/Transforms/RequestHeaderRouteValueTransform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;

namespace Yarp.ReverseProxy.Transforms;

public class RequestHeaderRouteValueTransform : RequestHeaderTransform
{
public RequestHeaderRouteValueTransform(string headerName, string routeValueKey, bool append)
: base(headerName, append)
{
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();
}
}

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

namespace Yarp.ReverseProxy.Transforms;

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

Append = append;
HeaderName = headerName;
}

internal bool Append { 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;
}

if (Append)
{
var existingValues = TakeHeader(context, HeaderName);
var newValue = StringValues.Concat(existingValues, value);
AddHeader(context, HeaderName, newValue);
}
else
{
RemoveHeader(context, HeaderName);
AddHeader(context, HeaderName, value);
}

return default;
}

protected abstract string? GetValue(RequestTransformContext context);
}

35 changes: 8 additions & 27 deletions src/ReverseProxy/Transforms/RequestHeaderValueTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,35 @@

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;

namespace Yarp.ReverseProxy.Transforms;

/// <summary>
/// Sets or appends simple request header values.
/// </summary>
public class RequestHeaderValueTransform : RequestTransform
public class RequestHeaderValueTransform : RequestHeaderTransform
{
public RequestHeaderValueTransform(string headerName, string value, bool append)
public RequestHeaderValueTransform(string headerName, string value, bool append) : base(headerName, append)
{
if (string.IsNullOrEmpty(headerName))
{
throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName));
}

HeaderName = headerName;
Value = value ?? throw new ArgumentNullException(nameof(value));
Append = append;
}

internal string HeaderName { get; }

internal string Value { get; }

internal bool Append { get; }

/// <inheritdoc/>
public override ValueTask ApplyAsync(RequestTransformContext context)
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

if (Append)
{
var existingValues = TakeHeader(context, HeaderName);
var values = StringValues.Concat(existingValues, Value);
AddHeader(context, HeaderName, values);
}
else
{
// Set
RemoveHeader(context, HeaderName);
AddHeader(context, HeaderName, Value);
}
return base.ApplyAsync(context);
}

return default;
/// <inheritdoc/>
protected override string GetValue(RequestTransformContext context)
{
return Value;
}
}
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 from a route value.
/// </summary>
public static RouteConfig WithTransformRequestHeaderRouteValue(this RouteConfig route, string headerName, string routeValueKey, bool append = true)
{
var type = append ? RequestHeadersTransformFactory.AppendKey : RequestHeadersTransformFactory.SetKey;
return route.WithTransform(transform =>
{
transform[RequestHeadersTransformFactory.RequestHeaderRouteValueKey] = 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,15 @@ 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 AddRequestHeaderRouteValue(this TransformBuilderContext context, string headerName, string routeValueKey, bool append = true)
{
context.RequestTransforms.Add(new RequestHeaderRouteValueTransform(headerName, routeValueKey, append));
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 RequestHeaderRouteValueKey = "RequestHeaderRouteValue";
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(RequestHeaderRouteValueKey, 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(RequestHeaderRouteValueKey, out var headerNameFromRoute))
{
TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);
if (transformValues.TryGetValue(AppendKey, out var routeValueKeyAppend))
{
context.AddRequestHeaderRouteValue(headerNameFromRoute, routeValueKeyAppend, append: true);
}
else if (transformValues.TryGetValue(SetKey, out var routeValueKeySet))
{
context.AddRequestHeaderRouteValue(headerNameFromRoute, 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 RequestHeaderRouteValueTransformTests
{
[Theory]
[InlineData("defaultHeader","value","/{a}/{b}/{c}", "a", "value;6", true)]
[InlineData("defaultHeader","value","/{a}/{b}/{c}", "notInRoute", "value", true)]
[InlineData("defaultHeader","value","/{a}/{b}/{c}", "notInRoute", "value", false)]
[InlineData("defaultHeader","value","/{a}/{b}/{c}", "a", "6", false)]
[InlineData("h1","value","/{a}/{b}/{c}", "a", "6", false)]
[InlineData("h1","value","/{a}/{b}/{c}", "b", "7", false)]
[InlineData("h1","value","/{a}/{*remainder}", "remainder", "7/8", false)]
public async Task AddsRequestHeaderRouteValue_SetHeader(string headerName, string defaultHeaderStartValue, string pattern, string routeValueKey, string expected, bool append)
{
// 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 RequestHeaderRouteValueTransform(headerName, routeValueKey, append);
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 WithTransformRequestHeaderRouteValue(bool append)
{
var routeConfig = new RouteConfig();
routeConfig = routeConfig.WithTransformRequestHeaderRouteValue("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 AddRequestHeaderRouteValue(bool append)
{
var builderContext = CreateBuilderContext();
builderContext.AddRequestHeaderRouteValue("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<RequestHeaderRouteValueTransform>(requestTransform);
Assert.Equal("key", requestHeaderFromRouteTransform.HeaderName);
Assert.Equal("value", requestHeaderFromRouteTransform.RouteValueKey);
var expectedMode = append;
Assert.Equal(expectedMode, requestHeaderFromRouteTransform.Append);
}
}
Loading