Skip to content

Commit 5a87acf

Browse files
CopilotJamesNKCopilot
authored
Add shared helper methods for localhost address validation (#12269)
Co-authored-by: JamesNK <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: James Newton-King <[email protected]>
1 parent af0f8c0 commit 5a87acf

File tree

4 files changed

+137
-4
lines changed

4 files changed

+137
-4
lines changed

src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Aspire.Hosting.ApplicationModel;
99
using Aspire.Hosting.DevTunnels;
1010
using Aspire.Hosting.Eventing;
11+
using Aspire.Hosting.Utils;
1112
using Microsoft.Extensions.DependencyInjection;
1213
using Microsoft.Extensions.DependencyInjection.Extensions;
1314
using Microsoft.Extensions.Diagnostics.HealthChecks;
@@ -466,8 +467,7 @@ private static void AddDevTunnelPort(
466467
.SingleOrDefault(a => StringComparers.EndpointAnnotationName.Equals(a.Name, targetEndpoint.EndpointName)) is { } targetEndpointAnnotation)
467468
{
468469
// The target endpoint already exists so let's ensure it's target is localhost
469-
if (!string.Equals(targetEndpointAnnotation.TargetHost, "localhost", StringComparison.OrdinalIgnoreCase)
470-
&& !targetEndpointAnnotation.TargetHost.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase))
470+
if (!EndpointHostHelpers.IsLocalhostOrLocalhostTld(targetEndpointAnnotation.TargetHost))
471471
{
472472
// Target endpoint is not localhost so can't be tunneled
473473
throw new ArgumentException($"Cannot tunnel endpoint '{targetEndpointAnnotation.Name}' with host '{targetEndpointAnnotation.TargetHost}' on resource '{targetResource.Name}' because it is not a localhost endpoint.", nameof(targetEndpoint));

src/Aspire.Hosting/Dcp/DcpExecutor.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,8 +1735,7 @@ private static (string, EndpointBindingMode) NormalizeTargetHost(string targetHo
17351735
return targetHost switch
17361736
{
17371737
null or "" => ("localhost", EndpointBindingMode.SingleAddress), // Default is localhost
1738-
var s when string.Equals(s, "localhost", StringComparison.OrdinalIgnoreCase) => ("localhost", EndpointBindingMode.SingleAddress), // Explicitly set to localhost
1739-
var s when s.Length > 10 && s.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase) => ("localhost", EndpointBindingMode.SingleAddress), // Explicitly set to localhost when using .localhost subdomain
1738+
var s when EndpointHostHelpers.IsLocalhostOrLocalhostTld(s) => ("localhost", EndpointBindingMode.SingleAddress), // Explicitly set to localhost when using localhost or .localhost subdomain
17401739
var s when IPAddress.TryParse(s, out var ipAddress) => ipAddress switch // The host is an IP address
17411740
{
17421741
var ip when IPAddress.Any.Equals(ip) => ("localhost", EndpointBindingMode.IPv4AnyAddresses), // 0.0.0.0 (IPv4 all addresses)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Aspire.Hosting.Utils;
5+
6+
/// <summary>
7+
/// Provides helper methods for validating localhost addresses.
8+
/// </summary>
9+
public static class EndpointHostHelpers
10+
{
11+
/// <summary>
12+
/// Determines whether the specified host is "localhost".
13+
/// </summary>
14+
/// <param name="host">The host to check.</param>
15+
/// <returns>
16+
/// <c>true</c> if the host is "localhost" (case-insensitive); otherwise, <c>false</c>.
17+
/// </returns>
18+
public static bool IsLocalhost(string? host)
19+
{
20+
return host is not null && string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase);
21+
}
22+
23+
/// <summary>
24+
/// Determines whether the specified host ends with ".localhost".
25+
/// </summary>
26+
/// <param name="host">The host to check.</param>
27+
/// <returns>
28+
/// <c>true</c> if the host ends with ".localhost" (case-insensitive); otherwise, <c>false</c>.
29+
/// </returns>
30+
public static bool IsLocalhostTld(string? host)
31+
{
32+
return host is not null && host.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase);
33+
}
34+
35+
/// <summary>
36+
/// Determines whether the specified host is "localhost" or uses the ".localhost" top-level domain.
37+
/// </summary>
38+
/// <param name="host">The host to check.</param>
39+
/// <returns>
40+
/// <c>true</c> if the host is "localhost" (case-insensitive) or ends with ".localhost" (case-insensitive);
41+
/// otherwise, <c>false</c>.
42+
/// </returns>
43+
public static bool IsLocalhostOrLocalhostTld(string? host)
44+
{
45+
return IsLocalhost(host) || IsLocalhostTld(host);
46+
}
47+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.Utils;
5+
6+
namespace Aspire.Hosting.Tests.Utils;
7+
8+
public class EndpointHostHelpersTests
9+
{
10+
[Theory]
11+
[InlineData("localhost", true)]
12+
[InlineData("LOCALHOST", true)]
13+
[InlineData("LocalHost", true)]
14+
[InlineData("LoCaLhOsT", true)]
15+
[InlineData("app.localhost", false)]
16+
[InlineData("api.localhost", false)]
17+
[InlineData("127.0.0.1", false)]
18+
[InlineData("::1", false)]
19+
[InlineData("example.com", false)]
20+
[InlineData("notlocalhost", false)]
21+
[InlineData("localhostx", false)]
22+
[InlineData("", false)]
23+
[InlineData(null, false)]
24+
public void IsLocalhost_VariousInputs_ReturnsExpectedResult(string? host, bool expected)
25+
{
26+
// Act
27+
var result = EndpointHostHelpers.IsLocalhost(host);
28+
29+
// Assert
30+
Assert.Equal(expected, result);
31+
}
32+
33+
[Theory]
34+
[InlineData("localhost", false)]
35+
[InlineData("app.localhost", true)]
36+
[InlineData("api.localhost", true)]
37+
[InlineData("my-service.localhost", true)]
38+
[InlineData("APP.LOCALHOST", true)]
39+
[InlineData("Api.LocalHost", true)]
40+
[InlineData("my-service.LOCALHOST", true)]
41+
[InlineData("a.b.c.localhost", true)]
42+
[InlineData("127.0.0.1", false)]
43+
[InlineData("::1", false)]
44+
[InlineData("example.com", false)]
45+
[InlineData("localhost.example.com", false)]
46+
[InlineData("notlocalhost", false)]
47+
[InlineData("localhostx", false)]
48+
[InlineData("", false)]
49+
[InlineData(null, false)]
50+
public void IsLocalhostTld_VariousInputs_ReturnsExpectedResult(string? host, bool expected)
51+
{
52+
// Act
53+
var result = EndpointHostHelpers.IsLocalhostTld(host);
54+
55+
// Assert
56+
Assert.Equal(expected, result);
57+
}
58+
59+
[Theory]
60+
[InlineData("localhost", true)]
61+
[InlineData("LOCALHOST", true)]
62+
[InlineData("LocalHost", true)]
63+
[InlineData("LoCaLhOsT", true)]
64+
[InlineData("app.localhost", true)]
65+
[InlineData("api.localhost", true)]
66+
[InlineData("my-service.localhost", true)]
67+
[InlineData("APP.LOCALHOST", true)]
68+
[InlineData("Api.LocalHost", true)]
69+
[InlineData("my-service.LOCALHOST", true)]
70+
[InlineData("a.b.c.localhost", true)]
71+
[InlineData("127.0.0.1", false)]
72+
[InlineData("::1", false)]
73+
[InlineData("example.com", false)]
74+
[InlineData("localhost.example.com", false)]
75+
[InlineData("notlocalhost", false)]
76+
[InlineData("localhostx", false)]
77+
[InlineData("", false)]
78+
[InlineData(null, false)]
79+
public void IsLocalhostOrLocalhostTld_VariousInputs_ReturnsExpectedResult(string? host, bool expected)
80+
{
81+
// Act
82+
var result = EndpointHostHelpers.IsLocalhostOrLocalhostTld(host);
83+
84+
// Assert
85+
Assert.Equal(expected, result);
86+
}
87+
}

0 commit comments

Comments
 (0)