diff --git a/src/Components/Server/src/Builder/InternalServerRenderMode.cs b/src/Components/Server/src/Builder/InternalServerRenderMode.cs index f5c8883879a65..eeed3a688f4ed 100644 --- a/src/Components/Server/src/Builder/InternalServerRenderMode.cs +++ b/src/Components/Server/src/Builder/InternalServerRenderMode.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.Server; -internal class InternalServerRenderMode(ServerComponentsEndpointOptions options = null) : InteractiveServerRenderMode +internal class InternalServerRenderMode(ServerComponentsEndpointOptions options) : InteractiveServerRenderMode { - public ServerComponentsEndpointOptions? Options { get; } = options ?? new() { EnableWebSocketCompression = true }; + public ServerComponentsEndpointOptions? Options { get; } = options; } diff --git a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs index 23f50bace580f..34bcd8d16de8c 100644 --- a/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs +++ b/src/Components/Server/src/Builder/ServerComponentsEndpointOptions.cs @@ -14,7 +14,7 @@ public class ServerComponentsEndpointOptions /// /// Gets or sets a value that indicates whether compression is enabled for the WebSocket connections. /// - public bool EnableWebSocketCompression { get; set; } + public bool EnableWebSocketCompression { get; set; } = true; /// /// Gets or sets the frame-ancestors Content-Security-Policy to set in the @@ -41,7 +41,8 @@ public class ServerComponentsEndpointOptions /// /// Gets or sets a callback to configure the underlying . - /// If set, this callback takes precedence over . + /// If set, will be applied independent of the value of + /// . /// public Action ConnectionOptions { get; set; } } diff --git a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs index ebe73366da464..12ebff1e31951 100644 --- a/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ServerRazorComponentsEndpointConventionBuilderExtensions.cs @@ -38,7 +38,7 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveServerRende ComponentEndpointConventionBuilderHelper.AddRenderMode(builder, new InternalServerRenderMode(options)); - if (options.EnableWebSocketCompression && options.ContentSecurityFrameAncestorPolicy != null) + if ((options.EnableWebSocketCompression || options.ConnectionOptions is not null) && options.ContentSecurityFrameAncestorPolicy != null) { builder.AddEndpointFilter(new RequireCspFilter(options.ContentSecurityFrameAncestorPolicy)); } diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs index 5bd6846cf3b93..e39b3b8ffcf54 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs @@ -3,6 +3,7 @@ using System.Reflection; using Microsoft.AspNetCore.E2ETesting; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; @@ -17,6 +18,8 @@ public class AspNetSiteServerFixture : WebHostServerFixture public BuildWebHost BuildWebHostMethod { get; set; } + public Action UpdateHostServices { get; set; } + public GetContentRoot GetContentRootMethod { get; set; } = DefaultGetContentRoot; public AspNetEnvironment Environment { get; set; } = AspNetEnvironment.Production; @@ -40,12 +43,16 @@ protected override IHost CreateWebHost() host = E2ETestOptions.Instance.Sauce.HostName; } - return BuildWebHostMethod(new[] + var result = BuildWebHostMethod(new[] { "--urls", $"http://{host}:0", "--contentroot", sampleSitePath, "--environment", Environment.ToString(), }.Concat(AdditionalArguments).ToArray()); + + UpdateHostServices?.Invoke(result.Services); + + return result; } private static string DefaultGetContentRoot(Assembly assembly) diff --git a/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs new file mode 100644 index 0000000000000..9589fb1d2a3cc --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/WebSocketCompressionTests.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestPlatform.Utilities; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests; + +public abstract partial class AllowedWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : ServerTestBase>>(browserFixture, serverFixture, output) +{ + [Fact] + public void EmbeddingServerAppInsideIframe_Works() + { + Navigate("/subdir/iframe"); + + var logs = Browser.GetBrowserLogs(LogLevel.Severe); + + Assert.Empty(logs); + + // Get the iframe element from the page, and inspect its contents for a p element with id inside-iframe + var iframe = Browser.FindElement(By.TagName("iframe")); + Browser.SwitchTo().Frame(iframe); + Browser.Exists(By.Id("inside-iframe")); + } +} + +public abstract partial class BlockedWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : ServerTestBase>>(browserFixture, serverFixture, output) +{ + [Fact] + public void EmbeddingServerAppInsideIframe_WithCompressionEnabled_Fails() + { + Navigate("/subdir/iframe"); + + var logs = Browser.GetBrowserLogs(LogLevel.Severe); + + Assert.True(logs.Count > 0); + + Assert.Matches(ParseErrorMessage(), logs[0].Message); + } + + [GeneratedRegex(@"security - Refused to frame 'http://\d+\.\d+\.\d+\.\d+:\d+/' because an ancestor violates the following Content Security Policy directive: ""frame-ancestors 'none'"".")] + private static partial Regex ParseErrorMessage(); +} + +public partial class DefaultConfigurationWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : BlockedWebSocketCompressionTests(browserFixture, serverFixture, output) +{ +} + +public partial class CustomConfigurationCallbackWebSocketCompressionTests : BlockedWebSocketCompressionTests +{ + public CustomConfigurationCallbackWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) : base(browserFixture, serverFixture, output) + { + serverFixture.UpdateHostServices = services => + { + var configuration = services.GetService(); + configuration.ConnectionDispatcherOptions = options => + options.WebSockets.WebSocketAcceptContextFactory = context => + new Http.WebSocketAcceptContext { DangerousEnableCompression = true }; + }; + } +} + +public partial class CompressionDisabledWebSocketCompressionTests : AllowedWebSocketCompressionTests +{ + public CompressionDisabledWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) : base( + browserFixture, serverFixture, output) + { + serverFixture.UpdateHostServices = services => + { + var configuration = services.GetService(); + configuration.IsCompressionEnabled = false; + }; + } +} + +public partial class SelfFrameAncestorWebSocketCompressionTests : AllowedWebSocketCompressionTests +{ + public SelfFrameAncestorWebSocketCompressionTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + serverFixture.UpdateHostServices = services => + { + var configuration = services.GetService(); + configuration.CspPolicy = "'self'"; + }; + } +} + diff --git a/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs b/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs index 074c58201e61d..b5edbecff6ead 100644 --- a/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/BlazorWebServerStartup.cs @@ -20,6 +20,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddRazorComponents() .AddInteractiveServerComponents(); + services.AddSingleton(); // Since tests run in parallel, we use an ephemeral key provider to avoid filesystem // contention issues. @@ -42,7 +43,7 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, { endpoints.MapRazorComponents() .AddInteractiveWebAssemblyRenderMode() - .AddInteractiveServerRenderMode(options => options.EnableWebSocketCompression = true); + .AddInteractiveServerRenderMode(); }); }); } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index be68ef2ffd7e1..73f0d7fb9892b 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -36,6 +36,7 @@ public void ConfigureServices(IServiceCollection services) services.AddHttpContextAccessor(); services.AddSingleton(); services.AddCascadingAuthenticationState(); + services.AddSingleton(); var circuitContextAccessor = new TestCircuitContextAccessor(); services.AddSingleton(circuitContextAccessor); @@ -69,7 +70,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorComponents() .AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal")) - .AddInteractiveServerRenderMode() + .AddInteractiveServerRenderMode(options => + { + var config = app.ApplicationServices.GetRequiredService(); + options.EnableWebSocketCompression = config.IsCompressionEnabled; + options.ContentSecurityFrameAncestorPolicy = config.CspPolicy; + options.ConnectionOptions = config.ConnectionDispatcherOptions; + }) .AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmMinimal"); NotEnabledStreamingRenderingComponent.MapEndpoints(endpoints); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/CanNotEmbedAppInsideIFrameWhenUsingCompression.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/CanNotEmbedAppInsideIFrameWhenUsingCompression.razor new file mode 100644 index 0000000000000..99fe7c2646e23 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/CanNotEmbedAppInsideIFrameWhenUsingCompression.razor @@ -0,0 +1,3 @@ +@page "/iframe" + + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EmbeddedInsideIFrame.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EmbeddedInsideIFrame.razor new file mode 100644 index 0000000000000..fbb1940670b6d --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EmbeddedInsideIFrame.razor @@ -0,0 +1,4 @@ +@page "/embedded" +@rendermode Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer + +

This is some content embedded inside an iframe

diff --git a/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs new file mode 100644 index 0000000000000..662ef4551688e --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/WebSocketCompressionConfiguration.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Connections; + +namespace TestServer; + +public class WebSocketCompressionConfiguration +{ + public bool IsCompressionEnabled { get; set; } = true; + + public string CspPolicy { get; set; } = "'none'"; + + public Action ConnectionDispatcherOptions { get; set; } +}