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

[release/8.0] YARP: add special-case for localhost when setting Host value #4076

Merged
merged 1 commit into from
May 4, 2024
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
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