Skip to content

Commit 31ce95b

Browse files
authored
YARP: add special-case for localhost when setting Host value (dotnet#4069)
1 parent 6ac46a1 commit 31ce95b

File tree

2 files changed

+119
-14
lines changed

2 files changed

+119
-14
lines changed

src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs

+22-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ public async ValueTask<ResolvedDestinationCollection> ResolveDestinationsAsync(I
5555
CancellationToken cancellationToken)
5656
{
5757
var originalUri = new Uri(originalConfig.Address);
58-
var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority;
5958
var serviceName = originalUri.GetLeftPart(UriPartial.Authority);
6059

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

9291
var name = $"{originalName}[{addressString}]";
93-
var config = originalConfig with { Host = originalHost, Address = resolvedAddress, Health = healthAddress };
92+
string? resolvedHost;
93+
94+
// Use the configured 'Host' value if it is provided.
95+
if (!string.IsNullOrEmpty(originalConfig.Host))
96+
{
97+
resolvedHost = originalConfig.Host;
98+
}
99+
else if (uri.IsLoopback)
100+
{
101+
// If there is no configured 'Host' value and the address resolves to localhost, do not set a host.
102+
// This is to account for non-wildcard development certificate.
103+
resolvedHost = null;
104+
}
105+
else
106+
{
107+
// Excerpt from RFC 9110 Section 7.2: The "Host" header field in a request provides the host and port information from the target URI [...]
108+
// See: https://www.rfc-editor.org/rfc/rfc9110.html#field.host
109+
// i.e, use Authority and not Host.
110+
resolvedHost = originalUri.Authority;
111+
}
112+
113+
var config = originalConfig with { Host = resolvedHost, Address = resolvedAddress, Health = healthAddress };
94114
results.Add((name, config));
95115
}
96116

tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs

+97-12
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,22 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests;
1818
/// </summary>
1919
public class YarpServiceDiscoveryTests
2020
{
21+
private static ServiceDiscoveryDestinationResolver CreateResolver(IServiceProvider serviceProvider)
22+
{
23+
var coreResolver = serviceProvider.GetRequiredService<ServiceEndpointResolver>();
24+
return new ServiceDiscoveryDestinationResolver(
25+
coreResolver,
26+
serviceProvider.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
27+
}
28+
2129
[Fact]
2230
public async Task ServiceDiscoveryDestinationResolverTests_PassThrough()
2331
{
2432
await using var services = new ServiceCollection()
2533
.AddServiceDiscoveryCore()
2634
.AddPassThroughServiceEndpointProvider()
2735
.BuildServiceProvider();
28-
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
29-
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
36+
var yarpResolver = CreateResolver(services);
3037

3138
var destinationConfigs = new Dictionary<string, DestinationConfig>
3239
{
@@ -57,8 +64,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration()
5764
.AddServiceDiscoveryCore()
5865
.AddConfigurationServiceEndpointProvider()
5966
.BuildServiceProvider();
60-
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
61-
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
67+
var yarpResolver = CreateResolver(services);
6268

6369
var destinationConfigs = new Dictionary<string, DestinationConfig>
6470
{
@@ -88,8 +94,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref
8894
.AddServiceDiscoveryCore()
8995
.AddConfigurationServiceEndpointProvider()
9096
.BuildServiceProvider();
91-
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
92-
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
97+
var yarpResolver = CreateResolver(services);
9398

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

114+
[Theory]
115+
[InlineData(false)]
116+
[InlineData(true)]
117+
public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Host_Value(bool configHasHost)
118+
{
119+
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
120+
{
121+
["services:basket:default:0"] = "https://localhost:1111",
122+
["services:basket:default:1"] = "https://127.0.0.1:2222",
123+
["services:basket:default:2"] = "https://[::1]:3333",
124+
["services:basket:default:3"] = "https://baskets-galore.faketld",
125+
});
126+
await using var services = new ServiceCollection()
127+
.AddSingleton<IConfiguration>(config.Build())
128+
.AddServiceDiscoveryCore()
129+
.AddConfigurationServiceEndpointProvider()
130+
.BuildServiceProvider();
131+
var yarpResolver = CreateResolver(services);
132+
133+
var destinationConfigs = new Dictionary<string, DestinationConfig>
134+
{
135+
["dest-a"] = new()
136+
{
137+
Address = "https://basket",
138+
Host = configHasHost ? "my-basket-svc.faketld" : null
139+
},
140+
};
141+
142+
var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None);
143+
144+
Assert.Equal(4, result.Destinations.Count);
145+
Assert.Collection(result.Destinations.Values,
146+
a =>
147+
{
148+
Assert.Equal("https://localhost:1111/", a.Address);
149+
if (configHasHost)
150+
{
151+
Assert.Equal("my-basket-svc.faketld", a.Host);
152+
}
153+
else
154+
{
155+
Assert.Null(a.Host);
156+
}
157+
},
158+
a =>
159+
{
160+
Assert.Equal("https://127.0.0.1:2222/", a.Address);
161+
if (configHasHost)
162+
{
163+
Assert.Equal("my-basket-svc.faketld", a.Host);
164+
}
165+
else
166+
{
167+
Assert.Null(a.Host);
168+
}
169+
},
170+
a =>
171+
{
172+
Assert.Equal("https://[::1]:3333/", a.Address);
173+
if (configHasHost)
174+
{
175+
Assert.Equal("my-basket-svc.faketld", a.Host);
176+
}
177+
else
178+
{
179+
Assert.Null(a.Host);
180+
}
181+
},
182+
a =>
183+
{
184+
Assert.Equal("https://baskets-galore.faketld/", a.Address);
185+
if (configHasHost)
186+
{
187+
Assert.Equal("my-basket-svc.faketld", a.Host);
188+
}
189+
else
190+
{
191+
// For non-localhost values, fallback to the input address.
192+
Assert.Equal("basket", a.Host);
193+
}
194+
});
195+
}
196+
109197
[Fact]
110198
public async Task ServiceDiscoveryDestinationResolverTests_Configuration_DisallowedScheme()
111199
{
@@ -125,8 +213,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo
125213
})
126214
.AddConfigurationServiceEndpointProvider()
127215
.BuildServiceProvider();
128-
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
129-
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
216+
var yarpResolver = CreateResolver(services);
130217

131218
var destinationConfigs = new Dictionary<string, DestinationConfig>
132219
{
@@ -149,8 +236,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Dns()
149236
.AddServiceDiscoveryCore()
150237
.AddDnsServiceEndpointProvider()
151238
.BuildServiceProvider();
152-
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
153-
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
239+
var yarpResolver = CreateResolver(services);
154240

155241
var destinationConfigs = new Dictionary<string, DestinationConfig>
156242
{
@@ -209,8 +295,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv()
209295
.AddServiceDiscoveryCore()
210296
.AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns")
211297
.BuildServiceProvider();
212-
var coreResolver = services.GetRequiredService<ServiceEndpointResolver>();
213-
var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>());
298+
var yarpResolver = CreateResolver(services);
214299

215300
var destinationConfigs = new Dictionary<string, DestinationConfig>
216301
{

0 commit comments

Comments
 (0)