Skip to content
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 @@ -22,6 +22,42 @@ internal sealed class AzureAppServiceEnvironmentContext(
public IServiceProvider ServiceProvider => serviceProvider;

private readonly Dictionary<IResource, AzureAppServiceWebsiteContext> _appServices = new(new ResourceNameComparer());
private readonly List<(string ResourceName, string[] EndpointNames)> _upgradedEndpoints = [];
private bool _hasLoggedHttpsUpgrade;

/// <summary>
/// Records HTTP endpoints that were upgraded to HTTPS for a resource.
/// </summary>
public void RecordHttpsUpgrade(string resourceName, string[] endpointNames)
{
if (endpointNames.Length > 0)
{
_upgradedEndpoints.Add((resourceName, endpointNames));
}
}

/// <summary>
/// Logs a single message about all HTTP endpoints that were upgraded to HTTPS.
/// </summary>
public void LogHttpsUpgradeIfNeeded()
{
if (_hasLoggedHttpsUpgrade || _upgradedEndpoints.Count == 0)
{
return;
}

_hasLoggedHttpsUpgrade = true;

var details = string.Join(", ", _upgradedEndpoints.Select(x =>
x.EndpointNames.Length == 1
? $"{x.ResourceName}:{x.EndpointNames[0]}"
: $"{x.ResourceName}:{{{string.Join(", ", x.EndpointNames)}}}"));

Logger.LogInformation(
"HTTP endpoints will use HTTPS (port 443) in Azure App Service: {Details}. " +
"To opt out, use .WithHttpsUpgrade(false) on the app service environment.",
details);
}

public AzureAppServiceWebsiteContext GetAppServiceContext(IResource resource)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,32 @@ public static IResourceBuilder<AzureAppServiceEnvironmentResource> AddAzureAppSe
return appServiceEnvBuilder;
}

/// <summary>
/// Configures whether HTTP endpoints should be automatically upgraded to HTTPS for the Azure App Service environment.
/// By default, HTTP endpoints are upgraded to HTTPS for security and WebSocket compatibility.
/// </summary>
/// <param name="builder">The <see cref="IResourceBuilder{AzureAppServiceEnvironmentResource}"/> to configure.</param>
/// <param name="upgrade">Whether to upgrade HTTP endpoints to HTTPS. Default is true.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining additional configuration.</returns>
Comment thread
davidfowl marked this conversation as resolved.
/// <remarks>
/// When disabled (<c>false</c>), HTTP endpoints will use HTTP scheme and port 80 in Azure App Service.
/// Note that Azure App Service forces HTTP to HTTPS redirects at the platform level,
/// so disabling upgrade primarily affects connection strings generated for dependent resources.
/// </remarks>
/// <example>
/// Preserve HTTP endpoints instead of automatically upgrading them to HTTPS:
/// <code>
/// var appService = builder.AddAzureAppServiceEnvironment("appservice")
/// .WithHttpsUpgrade(false);
/// </code>
/// </example>
[AspireExport(Description = "Configures whether HTTP endpoints are automatically upgraded to HTTPS in Azure App Service")]
public static IResourceBuilder<AzureAppServiceEnvironmentResource> WithHttpsUpgrade(this IResourceBuilder<AzureAppServiceEnvironmentResource> builder, bool upgrade = true)
{
builder.Resource.PreserveHttpEndpoints = !upgrade;
return builder;
}
Comment thread
davidfowl marked this conversation as resolved.

/// <summary>
/// Configures whether the Aspire dashboard should be included in the Azure App Service environment.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ await context.ReportingStep.CompleteAsync(
/// </summary>
internal BicepOutputReference WebSiteSuffix => new("webSiteSuffix", this);

/// <summary>
/// When true, HTTP endpoints are not upgraded to HTTPS. Default is false (HTTP→HTTPS upgrade is enabled).
/// </summary>
internal bool PreserveHttpEndpoints { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the Aspire dashboard should be included in the container app environment.
/// Default is true.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken
ComputeEnvironment = appServiceEnvironment
});
}

// Log once about all HTTP endpoints upgraded to HTTPS
appServiceEnvironmentContext.LogHttpsUpgradeIfNeeded();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ private void ProcessEndpoints()
throw new NotSupportedException("App Service does not support resources with multiple external endpoints.");
}

var preserveHttp = environmentContext.Environment.PreserveHttpEndpoints;

foreach (var resolved in resolvedEndpoints)
{
var endpoint = resolved.Endpoint;
Expand All @@ -128,16 +130,32 @@ private void ProcessEndpoints()
throw new NotSupportedException($"The endpoint '{endpoint.Name}' on resource '{resource.Name}' is not external. App Service only supports external endpoints.");
}

// By default, HTTP endpoints are upgraded to HTTPS in App Service
// If PreserveHttpEndpoints is true, keep the original scheme
var scheme = preserveHttp ? endpoint.UriScheme : "https";
var port = scheme is "http" ? 80 : 443;

// For App Service, we ignore port mappings since ports are handled by the platform
// TargetPort is null only for default ProjectResource endpoints (container port decides)
_endpointMapping[endpoint.Name] = new(
Scheme: endpoint.UriScheme,
Scheme: scheme,
Host: HostName,
Port: endpoint.UriScheme == "https" ? 443 : 80,
Port: port,
TargetPort: resolved.TargetPort,
IsHttpIngress: true,
External: true); // All App Service endpoints are external
}

// Record HTTP endpoints being upgraded (logged once at environment level)
if (!preserveHttp)
{
var upgradedEndpoints = resolvedEndpoints
.Where(r => r.Endpoint.UriScheme is "http")
.Select(r => r.Endpoint.Name)
.ToArray();

environmentContext.RecordHttpsUpgrade(resource.Name, upgradedEndpoints);
}
}

private (object, SecretType) ProcessValue(object value, SecretType secretType = SecretType.None, object? parent = null, bool isSlot = false)
Expand Down
38 changes: 38 additions & 0 deletions tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,44 @@ await Verify(manifest.ToString(), "json")
.AppendContentAsFile(bicep, "bicep");
}

[Fact]
public async Task CanPreserveHttpSchemeUsingWithHttpsUpgrade()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);

builder.AddAzureAppServiceEnvironment("env")
.WithHttpsUpgrade(false);

var project1 = builder.AddProject<Project>("project1", launchProfileName: null)
.WithHttpsEndpoint(targetPort: 8000)
.WithHttpEndpoint(targetPort: 8000)
.WithExternalHttpEndpoints();

// Add a second project that references project1 so we can verify
// the resolved endpoint URLs use http:// (not upgraded to https://)
var project2 = builder.AddProject<Project>("project2", launchProfileName: null)
.WithHttpEndpoint(targetPort: 9000)
.WithExternalHttpEndpoints()
.WithReference(project1);

using var app = builder.Build();

await ExecuteBeforeStartHooksAsync(app, default);

var model = app.Services.GetRequiredService<DistributedApplicationModel>();

project2.Resource.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);

var resource = target?.DeploymentTarget as AzureProvisioningResource;

Assert.NotNull(resource);

var (manifest, bicep) = await GetManifestWithBicep(resource);

await Verify(manifest.ToString(), "json")
.AppendContentAsFile(bicep, "bicep");
}

[Fact]
public async Task AddAppServiceWithTargetPortMultipleEndpoints()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@description('The location for the resource(s) to be deployed.')
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param env_outputs_azure_container_registry_endpoint string
Expand Down Expand Up @@ -54,11 +54,11 @@ resource webapp 'Microsoft.Web/sites@2025-03-01' = {
}
{
name: 'PROJECT1_HTTP'
value: 'http://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
value: 'https://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
}
{
name: 'services__project1__http__0'
value: 'http://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
value: 'https://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
}
{
name: 'ASPIRE_ENVIRONMENT_NAME'
Expand All @@ -84,4 +84,4 @@ resource slotConfigNames 'Microsoft.Web/sites/config@2025-03-01' = {
]
}
parent: webapp
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@description('The location for the resource(s) to be deployed.')
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param env_outputs_azure_container_registry_endpoint string
Expand Down Expand Up @@ -64,11 +64,11 @@ resource webapp 'Microsoft.Web/sites@2025-03-01' = {
}
{
name: 'PROJECT1_HTTP'
value: 'http://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
value: 'https://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
}
{
name: 'services__project1__http__0'
value: 'http://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
value: 'https://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
}
{
name: 'ASPIRE_ENVIRONMENT_NAME'
Expand Down Expand Up @@ -129,4 +129,4 @@ resource slotConfigNames 'Microsoft.Web/sites/config@2025-03-01' = {
]
}
parent: webapp
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@description('The location for the resource(s) to be deployed.')
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param env_outputs_azure_container_registry_endpoint string
Expand Down Expand Up @@ -66,11 +66,11 @@ resource webapp 'Microsoft.Web/sites@2025-03-01' = {
}
{
name: 'PROJECT1_HTTP'
value: 'http://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
value: 'https://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
}
{
name: 'services__project1__http__0'
value: 'http://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
value: 'https://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
}
{
name: 'ASPIRE_ENVIRONMENT_NAME'
Expand Down Expand Up @@ -133,4 +133,4 @@ resource slotConfigNames 'Microsoft.Web/sites/config@2025-03-01' = {
]
}
parent: webapp
}
}
Loading
Loading