Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 23 additions & 1 deletion src/Aspire.Hosting/Dashboard/DashboardServiceHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ void ConfigureKestrel(KestrelServerOptions kestrelOptions)
kestrelOptions.Listen(IPAddress.Loopback, port: 0, ConfigureListen);
_logger.LogDebug("Resource service endpoint not configured. Listening on {Scheme}://127.0.0.1:<random>.", scheme);
}
else if (uri.IsLoopback)
else if (IsLocalResourceServiceEndpoint(uri))
{
// Listen on the requested localhost port.
kestrelOptions.ListenLocalhost(uri.Port, ConfigureListen);
Expand Down Expand Up @@ -178,6 +178,28 @@ internal static string ResolveScheme(Uri? configuredUri, bool allowUnsecuredTran
return allowUnsecuredTransport ? "http" : "https";
}

/// <summary>
/// Determines whether the resource service endpoint is scoped to the local machine.
/// </summary>
internal static bool IsLocalResourceServiceEndpoint(Uri uri)
{
if (uri.IsLoopback)
{
return true;
}

var host = uri.Host.EndsWith(".", StringComparison.Ordinal)
? uri.Host[..^1]
: uri.Host;

// RFC 6761 reserves "localhost." and names under it for loopback resolution:
// https://www.rfc-editor.org/rfc/rfc6761#section-6.3. Uri.IsLoopback only
// recognizes "localhost" itself, but Aspire-generated polyglot AppHosts can use
// scoped names like "myapp.dev.localhost".
return string.Equals(host, KnownHostNames.Localhost, StringComparison.OrdinalIgnoreCase)
|| host.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing helper methods for testing whether a host is localhost or dev.localhost, etc on EndpointHostHelpers

}

/// <summary>
/// Gets the URI upon which the resource service is listening.
/// </summary>
Expand Down
42 changes: 42 additions & 0 deletions tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,48 @@ await auto.WaitUntilAsync(s =>
await auto.WaitForSuccessPromptAsync(counter);
}

[CaptureWorkspaceOnFailure]
[Fact]
public async Task CreateAndRunPolyglotAppHostWithDevLocalhostUrls()
{
var repoRoot = CliE2ETestHelpers.GetRepoRoot();
var strategy = CliInstallStrategy.Detect(output.WriteLine);

var workspace = TemporaryWorkspace.Create(output);

using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);

var counter = new SequenceCounter();
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken);

await auto.PrepareDockerEnvironmentAsync(counter, workspace);
await auto.InstallAspireCliAsync(strategy, counter);

const string projectName = "PolyglotDevLocalhost";
await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.ExpressReact, useDevLocalhost: true);

await auto.RunCommandFailFastAsync($"cd {projectName}", counter);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to rebase. This has been replaced with RunCommandAsync

await auto.RunCommandFailFastAsync("grep -F 'ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL' aspire.config.json && grep -F 'polyglotdevlocalhost.dev.localhost' aspire.config.json", counter);

await auto.TypeAsync("aspire run");
await auto.EnterAsync();

await auto.WaitUntilAsync(s =>
{
if (s.ContainsText("Capability Error") ||
s.ContainsText("ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL must contain a local loopback address"))
{
throw new InvalidOperationException("Polyglot AppHost failed to start with a *.dev.localhost resource service endpoint.");
}

return s.ContainsText("Press CTRL+C to stop the AppHost and exit.");
}, timeout: TimeSpan.FromMinutes(3), description: "Press CTRL+C message for polyglot AppHost with *.dev.localhost URLs");

await auto.Ctrl().KeyAsync(Hex1bKey.C);
await auto.WaitForSuccessPromptAsync(counter);
}

[CaptureWorkspaceOnFailure]
[Fact]
public async Task LatestCliCanStartStableChannelAppHost()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,21 @@ public void ResolveScheme_ReturnsExpectedScheme(string? uriString, bool allowUns

Assert.Equal(expectedScheme, scheme);
}

[Theory]
[InlineData("https://localhost:5001", true)]
[InlineData("https://localhost.:5001", true)]
[InlineData("https://127.0.0.1:5001", true)]
[InlineData("https://[::1]:5001", true)]
[InlineData("https://myapp.dev.localhost:5001", true)]
[InlineData("https://myapp.dev.localhost.:5001", true)]
[InlineData("https://example.com:5001", false)]
[InlineData("https://localhost.example.com:5001", false)]
[InlineData("https://example-localhost:5001", false)]
public void IsLocalResourceServiceEndpoint_ReturnsExpectedResult(string uriString, bool expectedResult)
{
var result = DashboardServiceHost.IsLocalResourceServiceEndpoint(new Uri(uriString));

Assert.Equal(expectedResult, result);
}
}
Loading