Skip to content

Commit

Permalink
YARP: add special-case for localhost when setting Host value (#4069)
Browse files Browse the repository at this point in the history
  • Loading branch information
ReubenBond authored May 3, 2024
1 parent 6ac46a1 commit 31ce95b
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ public async ValueTask<ResolvedDestinationCollection> ResolveDestinationsAsync(I
CancellationToken cancellationToken)
{
var originalUri = new Uri(originalConfig.Address);
var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority;
var serviceName = originalUri.GetLeftPart(UriPartial.Authority);

var result = await resolver.GetEndpointsAsync(serviceName, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -90,7 +89,28 @@ public async ValueTask<ResolvedDestinationCollection> ResolveDestinationsAsync(I
}

var name = $"{originalName}[{addressString}]";
var config = originalConfig with { Host = originalHost, Address = resolvedAddress, Health = healthAddress };
string? resolvedHost;

// Use the configured 'Host' value if it is provided.
if (!string.IsNullOrEmpty(originalConfig.Host))
{
resolvedHost = originalConfig.Host;
}
else if (uri.IsLoopback)
{
// If there is no configured 'Host' value and the address resolves to localhost, do not set a host.
// This is to account for non-wildcard development certificate.
resolvedHost = null;
}
else
{
// Excerpt from RFC 9110 Section 7.2: The "Host" header field in a request provides the host and port information from the target URI [...]
// See: https://www.rfc-editor.org/rfc/rfc9110.html#field.host
// i.e, use Authority and not Host.
resolvedHost = originalUri.Authority;
}

var config = originalConfig with { Host = resolvedHost, Address = resolvedAddress, Health = healthAddress };
results.Add((name, config));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,22 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests;
/// </summary>
public class YarpServiceDiscoveryTests
{
private static ServiceDiscoveryDestinationResolver CreateResolver(IServiceProvider serviceProvider)
{
var coreResolver = serviceProvider.GetRequiredService<ServiceEndpointResolver>();
return new ServiceDiscoveryDestinationResolver(
coreResolver,
serviceProvider.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
}

[Fact]
public async Task ServiceDiscoveryDestinationResolverTests_PassThrough()
{
await using var services = new ServiceCollection()
.AddServiceDiscoveryCore()
.AddPassThroughServiceEndpointProvider()
.BuildServiceProvider();
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
var yarpResolver = CreateResolver(services);

var destinationConfigs = new Dictionary<string, DestinationConfig>
{
Expand Down Expand Up @@ -57,8 +64,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration()
.AddServiceDiscoveryCore()
.AddConfigurationServiceEndpointProvider()
.BuildServiceProvider();
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
var yarpResolver = CreateResolver(services);

var destinationConfigs = new Dictionary<string, DestinationConfig>
{
Expand Down Expand Up @@ -88,8 +94,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref
.AddServiceDiscoveryCore()
.AddConfigurationServiceEndpointProvider()
.BuildServiceProvider();
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
var yarpResolver = CreateResolver(services);

var destinationConfigs = new Dictionary<string, DestinationConfig>
{
Expand All @@ -106,6 +111,89 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref
a => Assert.Equal("http://localhost:1111/", a));
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Host_Value(bool configHasHost)
{
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["services:basket:default:0"] = "https://localhost:1111",
["services:basket:default:1"] = "https://127.0.0.1:2222",
["services:basket:default:2"] = "https://[::1]:3333",
["services:basket:default:3"] = "https://baskets-galore.faketld",
});
await using var services = new ServiceCollection()
.AddSingleton<IConfiguration>(config.Build())
.AddServiceDiscoveryCore()
.AddConfigurationServiceEndpointProvider()
.BuildServiceProvider();
var yarpResolver = CreateResolver(services);

var destinationConfigs = new Dictionary<string, DestinationConfig>
{
["dest-a"] = new()
{
Address = "https://basket",
Host = configHasHost ? "my-basket-svc.faketld" : null
},
};

var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None);

Assert.Equal(4, result.Destinations.Count);
Assert.Collection(result.Destinations.Values,
a =>
{
Assert.Equal("https://localhost:1111/", a.Address);
if (configHasHost)
{
Assert.Equal("my-basket-svc.faketld", a.Host);
}
else
{
Assert.Null(a.Host);
}
},
a =>
{
Assert.Equal("https://127.0.0.1:2222/", a.Address);
if (configHasHost)
{
Assert.Equal("my-basket-svc.faketld", a.Host);
}
else
{
Assert.Null(a.Host);
}
},
a =>
{
Assert.Equal("https://[::1]:3333/", a.Address);
if (configHasHost)
{
Assert.Equal("my-basket-svc.faketld", a.Host);
}
else
{
Assert.Null(a.Host);
}
},
a =>
{
Assert.Equal("https://baskets-galore.faketld/", a.Address);
if (configHasHost)
{
Assert.Equal("my-basket-svc.faketld", a.Host);
}
else
{
// For non-localhost values, fallback to the input address.
Assert.Equal("basket", a.Host);
}
});
}

[Fact]
public async Task ServiceDiscoveryDestinationResolverTests_Configuration_DisallowedScheme()
{
Expand All @@ -125,8 +213,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo
})
.AddConfigurationServiceEndpointProvider()
.BuildServiceProvider();
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
var yarpResolver = CreateResolver(services);

var destinationConfigs = new Dictionary<string, DestinationConfig>
{
Expand All @@ -149,8 +236,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Dns()
.AddServiceDiscoveryCore()
.AddDnsServiceEndpointProvider()
.BuildServiceProvider();
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
var yarpResolver = CreateResolver(services);

var destinationConfigs = new Dictionary<string, DestinationConfig>
{
Expand Down Expand Up @@ -209,8 +295,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv()
.AddServiceDiscoveryCore()
.AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns")
.BuildServiceProvider();
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
var yarpResolver = CreateResolver(services);

var destinationConfigs = new Dictionary<string, DestinationConfig>
{
Expand Down

0 comments on commit 31ce95b

Please sign in to comment.